Compare commits
47 commits
Author | SHA1 | Date | |
---|---|---|---|
f90a7cd66d | |||
426ab872dd | |||
6920f9ee24 | |||
1340b3167f | |||
7a0420bb46 | |||
4627ae91b3 | |||
01b7750259 | |||
a26b9cc63e | |||
060b5705c8 | |||
13d8ead58b | |||
9be13ac352 | |||
5192ba630c | |||
c9a8de14f9 | |||
511897164e | |||
f1209d3b6b | |||
0175877dcd | |||
be3d103791 | |||
d959325faa | |||
933f308215 | |||
07a4fbdc8c | |||
2907ceef3d | |||
7686d0af88 | |||
7077cf55fe | |||
cafbd63f98 | |||
6ec7434ab6 | |||
58569dee2e | |||
b957988957 | |||
099692c91b | |||
038ef3faa0 | |||
fdabed0ade | |||
79d1047e00 | |||
c4cc29805d | |||
b8ddfd8e54 | |||
08c372110c | |||
6de9f11a48 | |||
d6d2c89816 | |||
a7a7c8b4f9 | |||
551e4f7f86 | |||
68e3b078e4 | |||
22cafb347b | |||
a5433beca0 | |||
2fd39d6fdb | |||
c3843adf9b | |||
759799acdc | |||
762d1baea4 | |||
4b34f8cd5e | |||
3791b5ee48 |
48 changed files with 1250 additions and 1234 deletions
41
.forgejo/workflows/build-and-release.yaml
Normal file
41
.forgejo/workflows/build-and-release.yaml
Normal file
|
@ -0,0 +1,41 @@
|
|||
on: [push]
|
||||
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 ${{ secrets.PACKAGE_PUBLISH_USER }} --password-stdin git.janky.solutions
|
||||
- name: gather metadata for container image tags
|
||||
uses: https://github.com/docker/metadata-action@v5
|
||||
id: meta
|
||||
with:
|
||||
images: git.janky.solutions/finn/lockserver
|
||||
- name: build container image
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
file: Containerfile
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
||||
push: ${{ github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v') }}
|
||||
build-args: |
|
||||
VERSION_STRING=${{ env.GITHUB_REF_NAME }}
|
||||
- name: update hassio-addons
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
run: |
|
||||
mkdir ~/.ssh
|
||||
echo "${{ secrets.DEPLOY_SSH_KEY }}" >> ~/.ssh/id_ed25519
|
||||
chmod 600 ~/.ssh/id_ed25519
|
||||
echo "git.janky.solutions ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDst5sxtaG74H62tHFyrxVyNhMmMNmvu/nXO/TqCaHoz1IbmKL6XEyOTC1Qy78GmCn1DVlZwx+pzEOed9s8REgu2j6DMeZ/qZ2+KBlsboSChdvppzXJj+7TrIdl2rqllAm3aJgKOXGkQdtd8rqaD62rzdaP/rt92Vp1DEavUORMCZ2sP0m7Etoj7FtUStaTMy/aUD+RtBdcZm+vV0xer0G46NSc4q127XTW2ZKwLrRZzve2NUo26Vkfnykgho5G2l5zvw7Zd6S9Rn+WdiQ5HE+cMmTU+AY2CAX8iPBhusDiuLc6Rou/ptqDOnZq2zggXMWJeeE31m5Oz2q6n26Ef7tbCzFgaZ34lNwp3EFzmpHetnNaFKx3z8sd9iGuWvk3evWFjXRAT382wKiIYKe8b3hgiizEP7eY0Hhey/A6Iv3PcQVr7DVr/BVzbWDZxPXtOJprCFgfgd9KOHcV5s3UD5k4+pNdgI+HuvAC13fv0rTxCshHMBWfgSofJ+zkqt+Jc9M=" >> ~/.ssh/known_hosts
|
||||
|
||||
set -ex
|
||||
git clone git@git.janky.solutions:finn/hassio-addons.git
|
||||
cd hassio-addons
|
||||
git config user.name "${GITHUB_REPOSITORY}"
|
||||
git config user.email "${GITHUB_REPOSITORY}@noreply.git.janky.solutions"
|
||||
sed -i "s#version: v.*#version: ${GITHUB_REF_NAME}#" lockserver/config.yaml
|
||||
git commit -am "bump lockserver to ${GITHUB_REF_NAME}"
|
||||
git push
|
|
@ -1,22 +0,0 @@
|
|||
on: [push]
|
||||
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
|
||||
- uses: https://github.com/docker/metadata-action@v5
|
||||
id: meta
|
||||
with:
|
||||
images: git.janky.solutions/finn/lockserver
|
||||
- name: build container image
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
file: Containerfile
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
||||
push: true
|
|
@ -14,10 +14,12 @@
|
|||
# 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
|
||||
FROM alpine:latest AS build
|
||||
RUN apk add --no-cache go
|
||||
ADD . /go/lockserver
|
||||
WORKDIR /go/lockserver
|
||||
RUN CGO_ENABLED=0 go build .
|
||||
ARG VERSION_STRING
|
||||
RUN CGO_ENABLED=0 go build -ldflags "-X git.janky.solutions/finn/lockserver/config.Version=${VERSION_STRING}" ./cmd/lockserver
|
||||
|
||||
FROM scratch
|
||||
COPY --from=build /go/lockserver/lockserver /lockserver
|
||||
|
|
18
README.md
Normal file
18
README.md
Normal file
|
@ -0,0 +1,18 @@
|
|||
# lockserver
|
||||
|
||||
_better Z-Wave Lock management for Home Assistant_
|
||||
|
||||
## Status
|
||||
|
||||
This is a work in progress. I have some ideas of where I want it to go, but I'm mostly experimenting with my own needs.
|
||||
|
||||
## Install
|
||||
|
||||
To add to Home Assistant, add my hassio-addons repo by clicking the button below, then search for and install the "LockServer" addon.
|
||||
|
||||
[](https://my.home-assistant.io/redirect/supervisor_add_addon_repository/?repository_url=https%3A%2F%2Fgit.janky.solutions%2Ffinn%2Fhassio-addons)
|
||||
|
||||
|
||||
## Usage
|
||||
|
||||
When you open the addon's web UI, it will show a list of Z-Wave locks. Clicking a lock shows all codes slots for that lock. Clicking edit on each slot allows changing the code, changing the name, enabling or disabling the slot, and seeing a log of recent uses of that code.
|
|
@ -1,27 +1,25 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"os"
|
||||
|
||||
"github.com/gorilla/securecookie"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
ZWaveJSServer string `json:"zwave-js-server"`
|
||||
SqliteDatabase string `json:"sqlite-database"`
|
||||
HTTPBind string `json:"http-bind"`
|
||||
SessionSecrets []JSONBytes `json:"session-secrets"`
|
||||
ZWaveJSServer string `json:"zwave-js-server"`
|
||||
SqliteDatabase string `json:"sqlite-database"`
|
||||
HTTPBind string `json:"http-bind"`
|
||||
GeneratedCodeLength int `json:"generated-code-length"`
|
||||
}
|
||||
|
||||
var C = Config{
|
||||
ZWaveJSServer: "ws://home-assistant:3000",
|
||||
SqliteDatabase: "lockserver.db",
|
||||
HTTPBind: ":8080",
|
||||
SessionSecrets: []JSONBytes{},
|
||||
ZWaveJSServer: "ws://addon_core_zwave_js:3000",
|
||||
SqliteDatabase: "/data/lockserver.db",
|
||||
HTTPBind: ":8099",
|
||||
GeneratedCodeLength: 10,
|
||||
}
|
||||
|
||||
var configFiles = []string{"lockserver.json", "/etc/lockserver.json"}
|
||||
|
@ -38,14 +36,6 @@ func Load() error {
|
|||
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
|
||||
}
|
||||
|
||||
|
@ -62,11 +52,3 @@ func load(path string) error {
|
|||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c Config) GetSessionSecrets() [][]byte {
|
||||
var resp [][]byte
|
||||
for _, s := range c.SessionSecrets {
|
||||
resp = append(resp, s.AsByteArrayArray())
|
||||
}
|
||||
return resp
|
||||
}
|
||||
|
|
34
config/version.go
Normal file
34
config/version.go
Normal file
|
@ -0,0 +1,34 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"runtime/debug"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
var (
|
||||
BuildInfo *debug.BuildInfo
|
||||
Version string
|
||||
)
|
||||
|
||||
func init() {
|
||||
var ok bool
|
||||
BuildInfo, ok = debug.ReadBuildInfo()
|
||||
if !ok {
|
||||
logrus.Error("failed to read build info")
|
||||
return
|
||||
}
|
||||
|
||||
if Version == "" {
|
||||
for _, setting := range BuildInfo.Settings {
|
||||
if setting.Key == "vcs.revision" {
|
||||
Version = setting.Value
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if Version == "" {
|
||||
Version = "development"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -5,15 +5,17 @@ import (
|
|||
"database/sql"
|
||||
"embed"
|
||||
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
goose "github.com/pressly/goose/v3"
|
||||
"github.com/sirupsen/logrus"
|
||||
_ "modernc.org/sqlite"
|
||||
|
||||
"git.janky.solutions/finn/lockserver/config"
|
||||
)
|
||||
|
||||
const sqliteParams = "?_pragma=foreign_keys(1)&_time_format=sqlite"
|
||||
|
||||
func Get() (*Queries, *sql.DB, error) {
|
||||
db, err := sql.Open("sqlite3", config.C.SqliteDatabase)
|
||||
db, err := sql.Open("sqlite", config.C.SqliteDatabase+sqliteParams)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
@ -47,10 +49,11 @@ func Migrate() error {
|
|||
}
|
||||
|
||||
func NullString(s string) sql.NullString {
|
||||
return sql.NullString{
|
||||
Valid: s != "",
|
||||
String: s,
|
||||
}
|
||||
return sql.NullString{Valid: s != "", String: s}
|
||||
}
|
||||
|
||||
func NullInt64(i int64) sql.NullInt64 {
|
||||
return sql.NullInt64{Valid: true, Int64: i}
|
||||
}
|
||||
|
||||
type loggingDBTX struct {
|
||||
|
|
138
db/issued_codes.sql.go
Normal file
138
db/issued_codes.sql.go
Normal file
|
@ -0,0 +1,138 @@
|
|||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.20.0
|
||||
// source: issued_codes.sql
|
||||
|
||||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
)
|
||||
|
||||
const assignIssuedCodeSlot = `-- name: AssignIssuedCodeSlot :exec
|
||||
INSERT INTO issued_code_slots (issued_code, lock, slot) VALUES (?, ?, ?)
|
||||
`
|
||||
|
||||
type AssignIssuedCodeSlotParams struct {
|
||||
IssuedCode interface{}
|
||||
Lock interface{}
|
||||
Slot interface{}
|
||||
}
|
||||
|
||||
func (q *Queries) AssignIssuedCodeSlot(ctx context.Context, arg AssignIssuedCodeSlotParams) error {
|
||||
_, err := q.db.ExecContext(ctx, assignIssuedCodeSlot, arg.IssuedCode, arg.Lock, arg.Slot)
|
||||
return err
|
||||
}
|
||||
|
||||
const createIssuedCode = `-- name: CreateIssuedCode :one
|
||||
INSERT INTO issued_codes (name, code, start, end) VALUES (?, ?, ?, ?) RETURNING id
|
||||
`
|
||||
|
||||
type CreateIssuedCodeParams struct {
|
||||
Name sql.NullString
|
||||
Code string
|
||||
Start sql.NullTime
|
||||
End sql.NullTime
|
||||
}
|
||||
|
||||
func (q *Queries) CreateIssuedCode(ctx context.Context, arg CreateIssuedCodeParams) (int64, error) {
|
||||
row := q.db.QueryRowContext(ctx, createIssuedCode,
|
||||
arg.Name,
|
||||
arg.Code,
|
||||
arg.Start,
|
||||
arg.End,
|
||||
)
|
||||
var id int64
|
||||
err := row.Scan(&id)
|
||||
return id, err
|
||||
}
|
||||
|
||||
const deleteIssuedCode = `-- name: DeleteIssuedCode :exec
|
||||
DELETE FROM issued_codes WHERE id = ?
|
||||
`
|
||||
|
||||
func (q *Queries) DeleteIssuedCode(ctx context.Context, id int64) error {
|
||||
_, err := q.db.ExecContext(ctx, deleteIssuedCode, id)
|
||||
return err
|
||||
}
|
||||
|
||||
const getActiveCodes = `-- name: GetActiveCodes :many
|
||||
SELECT id, name, code, start, "end" FROM issued_codes WHERE start < datetime('now') AND end > datetime('now')
|
||||
`
|
||||
|
||||
func (q *Queries) GetActiveCodes(ctx context.Context) ([]IssuedCode, error) {
|
||||
rows, err := q.db.QueryContext(ctx, getActiveCodes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []IssuedCode
|
||||
for rows.Next() {
|
||||
var i IssuedCode
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.Name,
|
||||
&i.Code,
|
||||
&i.Start,
|
||||
&i.End,
|
||||
); 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 getAllIssuedCodes = `-- name: GetAllIssuedCodes :many
|
||||
SELECT id, name, code, start, "end" FROM issued_codes
|
||||
`
|
||||
|
||||
func (q *Queries) GetAllIssuedCodes(ctx context.Context) ([]IssuedCode, error) {
|
||||
rows, err := q.db.QueryContext(ctx, getAllIssuedCodes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []IssuedCode
|
||||
for rows.Next() {
|
||||
var i IssuedCode
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.Name,
|
||||
&i.Code,
|
||||
&i.Start,
|
||||
&i.End,
|
||||
); 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 unassignIssuedCodeSlot = `-- name: UnassignIssuedCodeSlot :exec
|
||||
DELETE FROM issued_code_slots WHERE issued_code = ? AND lock = ?
|
||||
`
|
||||
|
||||
type UnassignIssuedCodeSlotParams struct {
|
||||
IssuedCode interface{}
|
||||
Lock interface{}
|
||||
}
|
||||
|
||||
func (q *Queries) UnassignIssuedCodeSlot(ctx context.Context, arg UnassignIssuedCodeSlotParams) error {
|
||||
_, err := q.db.ExecContext(ctx, unassignIssuedCodeSlot, arg.IssuedCode, arg.Lock)
|
||||
return err
|
||||
}
|
|
@ -1,10 +0,0 @@
|
|||
package db
|
||||
|
||||
import "git.janky.solutions/finn/lockserver/openapi"
|
||||
|
||||
func (l LockCodeSlot) OpenAPI() openapi.LockCodeSlot {
|
||||
return openapi.LockCodeSlot{
|
||||
Code: l.Code,
|
||||
Enabled: l.Enabled,
|
||||
}
|
||||
}
|
|
@ -9,35 +9,23 @@ import (
|
|||
"context"
|
||||
)
|
||||
|
||||
const getLockCodeBySlot = `-- name: GetLockCodeBySlot :one
|
||||
SELECT id, lock, code, slot, name, enabled FROM lock_code_slots WHERE lock = ? AND slot = ?
|
||||
const countUsedSlots = `-- name: CountUsedSlots :one
|
||||
SELECT COUNT(*) FROM lock_code_slots WHERE lock = ? AND enabled = 1
|
||||
`
|
||||
|
||||
type GetLockCodeBySlotParams struct {
|
||||
Lock int64
|
||||
Slot int64
|
||||
func (q *Queries) CountUsedSlots(ctx context.Context, lock int64) (int64, error) {
|
||||
row := q.db.QueryRowContext(ctx, countUsedSlots, lock)
|
||||
var count int64
|
||||
err := row.Scan(&count)
|
||||
return count, err
|
||||
}
|
||||
|
||||
func (q *Queries) GetLockCodeBySlot(ctx context.Context, arg GetLockCodeBySlotParams) (LockCodeSlot, error) {
|
||||
row := q.db.QueryRowContext(ctx, getLockCodeBySlot, arg.Lock, arg.Slot)
|
||||
var i LockCodeSlot
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.Lock,
|
||||
&i.Code,
|
||||
&i.Slot,
|
||||
&i.Name,
|
||||
&i.Enabled,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getLockCodes = `-- name: GetLockCodes :many
|
||||
const getAllLockCodesByLock = `-- name: GetAllLockCodesByLock :many
|
||||
SELECT id, lock, code, slot, name, enabled FROM lock_code_slots WHERE lock = ?
|
||||
`
|
||||
|
||||
func (q *Queries) GetLockCodes(ctx context.Context, lock int64) ([]LockCodeSlot, error) {
|
||||
rows, err := q.db.QueryContext(ctx, getLockCodes, lock)
|
||||
func (q *Queries) GetAllLockCodesByLock(ctx context.Context, lock int64) ([]LockCodeSlot, error) {
|
||||
rows, err := q.db.QueryContext(ctx, getAllLockCodesByLock, lock)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -66,6 +54,104 @@ func (q *Queries) GetLockCodes(ctx context.Context, lock int64) ([]LockCodeSlot,
|
|||
return items, nil
|
||||
}
|
||||
|
||||
const getEmptySlot = `-- name: GetEmptySlot :one
|
||||
SELECT id, lock, code, slot, name, enabled FROM lock_code_slots WHERE lock = ? AND enabled = 0 LIMIT 1
|
||||
`
|
||||
|
||||
func (q *Queries) GetEmptySlot(ctx context.Context, lock int64) (LockCodeSlot, error) {
|
||||
row := q.db.QueryRowContext(ctx, getEmptySlot, lock)
|
||||
var i LockCodeSlot
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.Lock,
|
||||
&i.Code,
|
||||
&i.Slot,
|
||||
&i.Name,
|
||||
&i.Enabled,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getLockCodeBySlot = `-- name: GetLockCodeBySlot :one
|
||||
SELECT id, lock, code, slot, name, enabled FROM lock_code_slots WHERE lock = ? AND slot = ?
|
||||
`
|
||||
|
||||
type GetLockCodeBySlotParams struct {
|
||||
Lock int64
|
||||
Slot int64
|
||||
}
|
||||
|
||||
func (q *Queries) GetLockCodeBySlot(ctx context.Context, arg GetLockCodeBySlotParams) (LockCodeSlot, error) {
|
||||
row := q.db.QueryRowContext(ctx, getLockCodeBySlot, arg.Lock, arg.Slot)
|
||||
var i LockCodeSlot
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.Lock,
|
||||
&i.Code,
|
||||
&i.Slot,
|
||||
&i.Name,
|
||||
&i.Enabled,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getLockCodesByCode = `-- name: GetLockCodesByCode :many
|
||||
SELECT id, lock, code, slot, name, enabled FROM lock_code_slots WHERE code = ?
|
||||
`
|
||||
|
||||
func (q *Queries) GetLockCodesByCode(ctx context.Context, code string) ([]LockCodeSlot, error) {
|
||||
rows, err := q.db.QueryContext(ctx, getLockCodesByCode, code)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []LockCodeSlot
|
||||
for rows.Next() {
|
||||
var i LockCodeSlot
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.Lock,
|
||||
&i.Code,
|
||||
&i.Slot,
|
||||
&i.Name,
|
||||
&i.Enabled,
|
||||
); 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 updateCodeSlot = `-- name: UpdateCodeSlot :exec
|
||||
UPDATE lock_code_slots SET code = ?, enabled = ?, name = ? WHERE lock = ? AND slot = ?
|
||||
`
|
||||
|
||||
type UpdateCodeSlotParams struct {
|
||||
Code string
|
||||
Enabled bool
|
||||
Name string
|
||||
Lock int64
|
||||
Slot int64
|
||||
}
|
||||
|
||||
func (q *Queries) UpdateCodeSlot(ctx context.Context, arg UpdateCodeSlotParams) error {
|
||||
_, err := q.db.ExecContext(ctx, updateCodeSlot,
|
||||
arg.Code,
|
||||
arg.Enabled,
|
||||
arg.Name,
|
||||
arg.Lock,
|
||||
arg.Slot,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
const upsertCodeSlot = `-- name: UpsertCodeSlot :exec
|
||||
INSERT INTO lock_code_slots (lock, slot, code, enabled, name) VALUES (?, ?, ?, ?, "") ON CONFLICT (lock, slot) DO UPDATE SET code=excluded.code, enabled=excluded.enabled
|
||||
`
|
||||
|
|
|
@ -25,8 +25,46 @@ func (q *Queries) AddLogEntry(ctx context.Context, arg AddLogEntryParams) error
|
|||
return err
|
||||
}
|
||||
|
||||
const getLastLogForSlot = `-- name: GetLastLogForSlot :many
|
||||
SELECT lock, timestamp, state, code, issued_code FROM lock_log WHERE lock = ? AND code = ? ORDER BY timestamp DESC LIMIT 1
|
||||
`
|
||||
|
||||
type GetLastLogForSlotParams struct {
|
||||
Lock int64
|
||||
Code sql.NullInt64
|
||||
}
|
||||
|
||||
func (q *Queries) GetLastLogForSlot(ctx context.Context, arg GetLastLogForSlotParams) ([]LockLog, error) {
|
||||
rows, err := q.db.QueryContext(ctx, getLastLogForSlot, arg.Lock, arg.Code)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []LockLog
|
||||
for rows.Next() {
|
||||
var i LockLog
|
||||
if err := rows.Scan(
|
||||
&i.Lock,
|
||||
&i.Timestamp,
|
||||
&i.State,
|
||||
&i.Code,
|
||||
&i.IssuedCode,
|
||||
); 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 getLogForLock = `-- name: GetLogForLock :many
|
||||
SELECT lock, timestamp, state, code FROM lock_log WHERE lock = ? ORDER BY timestamp DESC
|
||||
SELECT lock, timestamp, state, code, issued_code FROM lock_log WHERE lock = ? ORDER BY timestamp DESC
|
||||
`
|
||||
|
||||
func (q *Queries) GetLogForLock(ctx context.Context, lock int64) ([]LockLog, error) {
|
||||
|
@ -43,6 +81,45 @@ func (q *Queries) GetLogForLock(ctx context.Context, lock int64) ([]LockLog, err
|
|||
&i.Timestamp,
|
||||
&i.State,
|
||||
&i.Code,
|
||||
&i.IssuedCode,
|
||||
); 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 getLogForSlot = `-- name: GetLogForSlot :many
|
||||
SELECT lock, timestamp, state, code, issued_code FROM lock_log WHERE lock = ? AND code = ? ORDER BY timestamp DESC LIMIT 100
|
||||
`
|
||||
|
||||
type GetLogForSlotParams struct {
|
||||
Lock int64
|
||||
Code sql.NullInt64
|
||||
}
|
||||
|
||||
func (q *Queries) GetLogForSlot(ctx context.Context, arg GetLogForSlotParams) ([]LockLog, error) {
|
||||
rows, err := q.db.QueryContext(ctx, getLogForSlot, arg.Lock, arg.Code)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []LockLog
|
||||
for rows.Next() {
|
||||
var i LockLog
|
||||
if err := rows.Scan(
|
||||
&i.Lock,
|
||||
&i.Timestamp,
|
||||
&i.State,
|
||||
&i.Code,
|
||||
&i.IssuedCode,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
@ -23,37 +23,31 @@ CREATE TABLE lock_log (
|
|||
lock INTEGER NOT NULL REFERENCES locks(id),
|
||||
timestamp DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
state TEXT NOT NULL,
|
||||
code INTEGER REFERENCES lock_code_slots(id)
|
||||
code INTEGER REFERENCES lock_code_slots(id),
|
||||
issued_code INTEGER REFERENCES issued_codes(id)
|
||||
);
|
||||
|
||||
CREATE TABLE users (
|
||||
CREATE TABLE issued_codes (
|
||||
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),
|
||||
CREATE TABLE issued_code_slots (
|
||||
issued_code REFERENCES issued_codes(id),
|
||||
lock REFERENCES locks(id),
|
||||
slot REFERENCES lock_code_slots(id),
|
||||
|
||||
UNIQUE (user_code, lock)
|
||||
UNIQUE (issued_code, lock)
|
||||
);
|
||||
-- +goose StatementEnd
|
||||
|
||||
-- +goose Down
|
||||
-- +goose StatementBegin
|
||||
DROP TABLE user_code_slots;
|
||||
DROP TABLE user_codes;
|
||||
DROP TABLE users;
|
||||
DROP TABLE issued_code_slots;
|
||||
DROP TABLE issued_codes;
|
||||
DROP TABLE lock_log;
|
||||
DROP TABLE lock_code_slots;
|
||||
DROP TABLE locks;
|
||||
|
|
43
db/models.go
43
db/models.go
|
@ -9,6 +9,20 @@ import (
|
|||
"time"
|
||||
)
|
||||
|
||||
type IssuedCode struct {
|
||||
ID int64
|
||||
Name sql.NullString
|
||||
Code string
|
||||
Start sql.NullTime
|
||||
End sql.NullTime
|
||||
}
|
||||
|
||||
type IssuedCodeSlot struct {
|
||||
IssuedCode interface{}
|
||||
Lock interface{}
|
||||
Slot interface{}
|
||||
}
|
||||
|
||||
type Lock struct {
|
||||
ID int64
|
||||
Name string
|
||||
|
@ -25,28 +39,9 @@ type LockCodeSlot struct {
|
|||
}
|
||||
|
||||
type LockLog struct {
|
||||
Lock int64
|
||||
Timestamp time.Time
|
||||
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{}
|
||||
Lock int64
|
||||
Timestamp time.Time
|
||||
State string
|
||||
Code sql.NullInt64
|
||||
IssuedCode sql.NullInt64
|
||||
}
|
||||
|
|
17
db/queries/issued_codes.sql
Normal file
17
db/queries/issued_codes.sql
Normal file
|
@ -0,0 +1,17 @@
|
|||
-- name: CreateIssuedCode :one
|
||||
INSERT INTO issued_codes (name, code, start, end) VALUES (?, ?, ?, ?) RETURNING id;
|
||||
|
||||
-- name: DeleteIssuedCode :exec
|
||||
DELETE FROM issued_codes WHERE id = ?;
|
||||
|
||||
-- name: AssignIssuedCodeSlot :exec
|
||||
INSERT INTO issued_code_slots (issued_code, lock, slot) VALUES (?, ?, ?);
|
||||
|
||||
-- name: UnassignIssuedCodeSlot :exec
|
||||
DELETE FROM issued_code_slots WHERE issued_code = ? AND lock = ?;
|
||||
|
||||
-- name: GetAllIssuedCodes :many
|
||||
SELECT * FROM issued_codes;
|
||||
|
||||
-- name: GetActiveCodes :many
|
||||
SELECT * FROM issued_codes WHERE start < datetime('now') AND end > datetime('now');
|
|
@ -1,8 +1,20 @@
|
|||
-- name: UpsertCodeSlot :exec
|
||||
INSERT INTO lock_code_slots (lock, slot, code, enabled, name) VALUES (?, ?, ?, ?, "") ON CONFLICT (lock, slot) DO UPDATE SET code=excluded.code, enabled=excluded.enabled;
|
||||
|
||||
-- name: UpdateCodeSlot :exec
|
||||
UPDATE lock_code_slots SET code = ?, enabled = ?, name = ? WHERE lock = ? AND slot = ?;
|
||||
|
||||
-- name: GetLockCodeBySlot :one
|
||||
SELECT * FROM lock_code_slots WHERE lock = ? AND slot = ?;
|
||||
|
||||
-- name: GetLockCodes :many
|
||||
-- name: GetAllLockCodesByLock :many
|
||||
SELECT * FROM lock_code_slots WHERE lock = ?;
|
||||
|
||||
-- name: GetLockCodesByCode :many
|
||||
SELECT * FROM lock_code_slots WHERE code = ?;
|
||||
|
||||
-- name: GetEmptySlot :one
|
||||
SELECT * FROM lock_code_slots WHERE lock = ? AND enabled = 0 LIMIT 1;
|
||||
|
||||
-- name: CountUsedSlots :one
|
||||
SELECT COUNT(*) FROM lock_code_slots WHERE lock = ? AND enabled = 1;
|
||||
|
|
|
@ -3,3 +3,9 @@ INSERT INTO lock_log (lock, state, code) VALUES (?, ?, ?);
|
|||
|
||||
-- name: GetLogForLock :many
|
||||
SELECT * FROM lock_log WHERE lock = ? ORDER BY timestamp DESC;
|
||||
|
||||
-- name: GetLogForSlot :many
|
||||
SELECT * FROM lock_log WHERE lock = ? AND code = ? ORDER BY timestamp DESC LIMIT 100;
|
||||
|
||||
-- name: GetLastLogForSlot :many
|
||||
SELECT * FROM lock_log WHERE lock = ? AND code = ? ORDER BY timestamp DESC LIMIT 1;
|
||||
|
|
|
@ -1,14 +0,0 @@
|
|||
-- 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;
|
|
@ -1,8 +0,0 @@
|
|||
-- name: CreateUser :exec
|
||||
INSERT INTO users (name) VALUES (?);
|
||||
|
||||
-- name: GetUserByName :one
|
||||
SELECT * FROM users WHERE name = ?;
|
||||
|
||||
-- name: GetUserByID :one
|
||||
SELECT * FROM users WHERE id = ?;
|
|
@ -1,27 +0,0 @@
|
|||
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
|
||||
}
|
|
@ -1,41 +0,0 @@
|
|||
// 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
|
||||
}
|
4
frontend/404.html
Normal file
4
frontend/404.html
Normal file
|
@ -0,0 +1,4 @@
|
|||
{{ template "header.html" . }}
|
||||
<h3>404</h3>
|
||||
<p>page could not be found, maybe you want to <a href="{{ .BaseURL }}/">return home</a>.</p>
|
||||
{{ template "footer.html" }}
|
6
frontend/500.html
Normal file
6
frontend/500.html
Normal file
|
@ -0,0 +1,6 @@
|
|||
{{ template "header.html" . }}
|
||||
<h3>Unexpected Error</h3>
|
||||
<p>An error occured while trying to handle your request, maybe you should <a href="{{ .BaseURL }}/">return home</a>.</p>
|
||||
<br />
|
||||
<pre>{{ .Error }}</pre>
|
||||
{{ template "footer.html" }}
|
9
frontend/add-code.html
Normal file
9
frontend/add-code.html
Normal file
|
@ -0,0 +1,9 @@
|
|||
{{ template "header.html" . }}
|
||||
<header>Add Code</header>
|
||||
<form method="post">
|
||||
<label for="name">Name: <input type="text" name="name" id="name" /></label><br />
|
||||
<label for="code">Code: <input type="text" name="code" id="code" /></label><br />
|
||||
<br />
|
||||
<input type="submit" value="add code" />
|
||||
</form>
|
||||
{{ template "footer.html" }}
|
7
frontend/footer.html
Normal file
7
frontend/footer.html
Normal file
|
@ -0,0 +1,7 @@
|
|||
</div>
|
||||
<footer>
|
||||
<code>{{ version }}</code>
|
||||
</footer>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
|
@ -4,6 +4,9 @@ import (
|
|||
"embed"
|
||||
"html/template"
|
||||
"io/fs"
|
||||
"time"
|
||||
|
||||
"git.janky.solutions/finn/lockserver/config"
|
||||
)
|
||||
|
||||
var (
|
||||
|
@ -14,11 +17,18 @@ var (
|
|||
//go:embed *.html
|
||||
templatesFS embed.FS
|
||||
Templates *template.Template
|
||||
|
||||
funcs = template.FuncMap{
|
||||
"version": func() string { return config.Version },
|
||||
"time_since": func(t time.Time) string { return time.Since(t).Round(time.Second).String() },
|
||||
}
|
||||
)
|
||||
|
||||
func init() {
|
||||
t := template.New("").Funcs(funcs)
|
||||
|
||||
var err error
|
||||
Templates, err = template.ParseFS(templatesFS, "*")
|
||||
Templates, err = t.ParseFS(templatesFS, "*")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
|
14
frontend/header.html
Normal file
14
frontend/header.html
Normal file
|
@ -0,0 +1,14 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<title>Better Z-Wave Locks for Home Assistant</title>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no" />
|
||||
<link rel="stylesheet" href="{{ .BaseURL }}/static-{{ version }}/main.css" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="wrapper">
|
||||
<a href="{{ $.BaseURL }}/">Home</a>
|
||||
<div id="main">
|
|
@ -1,29 +1,11 @@
|
|||
<!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>
|
||||
{{ template "header.html" . }}
|
||||
<h1>Better Z-Wave Locks</h1>
|
||||
<br /><br />
|
||||
<span class="table">
|
||||
{{ range .Locks }}
|
||||
<a class="table-row" href="{{ $.BaseURL }}/locks/{{ .ID }}">
|
||||
<p class="table-cell">{{ if eq .Name "" }}Lock #{{ .ZwaveDeviceID }}{{ else }}{{ .Name }}{{ end }}</p>
|
||||
</a>
|
||||
{{ end }}
|
||||
</span>
|
||||
{{ template "footer.html" }}
|
||||
|
|
28
frontend/lock-code-edit.html
Normal file
28
frontend/lock-code-edit.html
Normal file
|
@ -0,0 +1,28 @@
|
|||
{{ template "header.html" . }}
|
||||
<header>{{ if eq .Data.lock.Name "" }}Lock #{{ .Data.lock.ID }}{{ else }}{{ .Data.lock.Name }}{{ end }} Slot #{{ .Data.code.Slot }}</header>
|
||||
<br />
|
||||
<form method="post">
|
||||
Code: <input type="text" name="code" value="{{ .Data.code.Code }}" id="code" /> <a href="#" onclick="generateCode()">🔄</a><br />
|
||||
Name: <input type="text" name="name" value="{{ .Data.code.Name }}" /><br />
|
||||
Enabled: <input type="checkbox" name="enabled" {{ if .Data.code.Enabled }}checked{{ end }} /><br />
|
||||
<br />
|
||||
<input type="submit" value="save" />
|
||||
</form>
|
||||
<br /><br />
|
||||
|
||||
<ul>
|
||||
{{ range $_, $entry := .Data.log }}
|
||||
<li>{{ $entry.State }} (<i>{{ $entry.Timestamp | time_since }} ago</i>)</li>
|
||||
{{ end }}
|
||||
</ul>
|
||||
|
||||
<script type="text/javascript">
|
||||
function generateCode() {
|
||||
let code = "";
|
||||
while(code.length < 4) {
|
||||
code += Math.round(Math.random()*10);
|
||||
}
|
||||
document.querySelector('#code').value = code;
|
||||
}
|
||||
</script>
|
||||
{{ template "footer.html" }}
|
8
frontend/lock-edit.html
Normal file
8
frontend/lock-edit.html
Normal file
|
@ -0,0 +1,8 @@
|
|||
{{ template "header.html" . }}
|
||||
<header>Rename {{ if eq .Data.Name "" }}Lock #{{ .Data.ID }}{{ else }}{{ .Data.Name }}{{ end }}</header>
|
||||
<br />
|
||||
<form method="post">
|
||||
Name: <input type="text" name="name" value="{{ .Data.Name }}" /><br />
|
||||
<input type="submit" value="save" />
|
||||
</form>
|
||||
{{ template "footer.html" }}
|
20
frontend/lock.html
Normal file
20
frontend/lock.html
Normal file
|
@ -0,0 +1,20 @@
|
|||
{{ template "header.html" . }}
|
||||
<header>{{ if eq .Data.lock.Name "" }}Lock #{{ .Data.lock.ID }}{{ else }}{{ .Data.lock.Name }}{{ end }}</header>
|
||||
[ <a href="{{ $.BaseURL }}/locks/{{ .Data.lock.ID }}/edit">rename</a> ]<br />
|
||||
<br />
|
||||
<span class="table">
|
||||
<span class="table-row">
|
||||
<span class="table-cell">Name</span>
|
||||
<span class="table-cell">Code</span>
|
||||
</span>
|
||||
{{ range $_, $code := .Data.codes }}
|
||||
<a href="{{ $.BaseURL }}/locks/{{ $.Data.lock.ID }}/codes/{{ $code.Slot }}" class="table-row code-{{ if $code.Enabled }}enabled{{ else }}disabled{{ end }}">
|
||||
<span class="table-cell">{{ $code.Name }}</span>
|
||||
<span class="table-cell">{{ $code.Code }}</span>
|
||||
</a>
|
||||
{{ end }}
|
||||
</span>
|
||||
|
||||
<br /><br />
|
||||
|
||||
{{ template "footer.html" }}
|
|
@ -1,12 +1,53 @@
|
|||
html, body {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
background-color: #1c1c1c;
|
||||
color: #fff;
|
||||
font-family: sans-serif;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
#wrapper {
|
||||
min-height: calc(100% - 2em);
|
||||
display: grid;
|
||||
grid-template-rows: auto 1fr auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
row-gap: 1em;
|
||||
padding: 1em;
|
||||
margin: auto;
|
||||
max-width: 1000px;
|
||||
}
|
||||
|
||||
#main {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
header {
|
||||
font-size: 1.5em;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.code-enabled {
|
||||
background-color: #050;
|
||||
}
|
||||
|
||||
.table {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.table-row {
|
||||
border-bottom: solid #999 1px;
|
||||
/* background-color: #333; */
|
||||
height: 3em;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-around;
|
||||
}
|
||||
|
||||
.table-row:first {
|
||||
border-top: solid #999 1px;
|
||||
}
|
||||
|
|
44
go.mod
44
go.mod
|
@ -3,39 +3,37 @@ 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/failsafe-go/failsafe-go v0.6.9
|
||||
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
|
||||
github.com/oapi-codegen/runtime v1.1.1
|
||||
github.com/pressly/goose/v3 v3.19.2
|
||||
github.com/gorilla/websocket v1.5.3
|
||||
github.com/labstack/echo/v4 v4.12.0
|
||||
github.com/pressly/goose/v3 v3.23.0
|
||||
github.com/sirupsen/logrus v1.9.3
|
||||
modernc.org/sqlite v1.34.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.20.2 // indirect
|
||||
github.com/go-openapi/swag v0.22.8 // indirect
|
||||
github.com/invopop/yaml v0.2.0 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
|
||||
github.com/labstack/gommon v0.4.2 // indirect
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mfridman/interpolate v0.0.2 // indirect
|
||||
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
|
||||
github.com/perimeterx/marshmallow v1.1.5 // indirect
|
||||
github.com/sethvargo/go-retry v0.2.4 // indirect
|
||||
github.com/ncruces/go-strftime v0.1.9 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
github.com/sethvargo/go-retry v0.3.0 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/valyala/fasttemplate v1.2.2 // indirect
|
||||
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.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
|
||||
golang.org/x/crypto v0.28.0 // indirect
|
||||
golang.org/x/net v0.30.0 // indirect
|
||||
golang.org/x/sync v0.9.0 // indirect
|
||||
golang.org/x/sys v0.26.0 // indirect
|
||||
golang.org/x/text v0.19.0 // indirect
|
||||
modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 // indirect
|
||||
modernc.org/libc v1.55.3 // indirect
|
||||
modernc.org/mathutil v1.6.0 // indirect
|
||||
modernc.org/memory v1.8.0 // indirect
|
||||
modernc.org/strutil v1.2.0 // indirect
|
||||
modernc.org/token v1.1.0 // indirect
|
||||
)
|
||||
|
|
245
go.sum
245
go.sum
|
@ -1,246 +1,101 @@
|
|||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
||||
github.com/ClickHouse/ch-go v0.58.2 h1:jSm2szHbT9MCAB1rJ3WuCJqmGLi5UTjlNu+f530UTS0=
|
||||
github.com/ClickHouse/ch-go v0.58.2/go.mod h1:Ap/0bEmiLa14gYjCiRkYGbXvbe8vwdrfTYWhsuQ99aw=
|
||||
github.com/ClickHouse/clickhouse-go/v2 v2.17.1 h1:ZCmAYWpu75IyEi7+Yrs/uaAjiCGY5wfW5kXo64exkX4=
|
||||
github.com/ClickHouse/clickhouse-go/v2 v2.17.1/go.mod h1:rkGTvFDTLqLIm0ma+13xmcCfr/08Gvs7KmFt1tgiWHQ=
|
||||
github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow=
|
||||
github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM=
|
||||
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw=
|
||||
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk=
|
||||
github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk=
|
||||
github.com/andybalholm/brotli v1.0.6 h1:Yf9fFpf49Zrxb9NlQaluyE92/+X7UVHlhMNJN2sxfOI=
|
||||
github.com/andybalholm/brotli v1.0.6/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
|
||||
github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230512164433-5d1fd1a340c9 h1:goHVqTbFX3AIo0tzGr14pgfAW2ZfPChKO21Z9MGf/gk=
|
||||
github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230512164433-5d1fd1a340c9/go.mod h1:pSwJ0fSY5KhvocuWSx4fz3BA8OrA1bQn+K1Eli3BRwM=
|
||||
github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ=
|
||||
github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk=
|
||||
github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w=
|
||||
github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM=
|
||||
github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||
github.com/containerd/continuity v0.4.3 h1:6HVkalIp+2u1ZLH1J/pYX2oBVXlJZvh1X1A7bEZ9Su8=
|
||||
github.com/containerd/continuity v0.4.3/go.mod h1:F6PTNCKepoxEaXLQp3wDAjygEnImnZ/7o4JzpodfroQ=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/docker/cli v24.0.7+incompatible h1:wa/nIwYFW7BVTGa7SWPVyyXU9lgORqUb1xfI36MSkFg=
|
||||
github.com/docker/cli v24.0.7+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||
github.com/docker/docker v24.0.7+incompatible h1:Wo6l37AuwP3JaMnZa226lzVXGA3F9Ig1seQen0cKYlM=
|
||||
github.com/docker/docker v24.0.7+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ=
|
||||
github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
|
||||
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
||||
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/elastic/go-sysinfo v1.11.2 h1:mcm4OSYVMyws6+n2HIVMGkln5HOpo5Ie1ZmbbNn0jg4=
|
||||
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=
|
||||
github.com/go-faster/city v1.0.1/go.mod h1:jKcUJId49qdW3L1qKHH/3wPeUstCVpVSXTM6vO3VcTw=
|
||||
github.com/go-faster/errors v0.6.1 h1:nNIPOBkprlKzkThvS/0YaX8Zs9KewLCOSFQS5BU06FI=
|
||||
github.com/go-faster/errors v0.6.1/go.mod h1:5MGV2/2T9yvlrbhe9pD9LO5Z/2zCSq2T8j+Jpi2LAyY=
|
||||
github.com/go-openapi/jsonpointer v0.20.2 h1:mQc3nmndL8ZBzStEo3JYF8wzmeWffDH4VbXz58sAx6Q=
|
||||
github.com/go-openapi/jsonpointer v0.20.2/go.mod h1:bHen+N0u1KEO3YlmqOjTT9Adn1RfD91Ar825/PuiRVs=
|
||||
github.com/go-openapi/swag v0.22.8 h1:/9RjDSQ0vbFR+NyjGMkFTsA1IA0fmhKSThmfGZjicbw=
|
||||
github.com/go-openapi/swag v0.22.8/go.mod h1:6QT22icPLEqAM/z/TChgb4WAveCHF92+2gF0CNjHpPI=
|
||||
github.com/go-sql-driver/mysql v1.8.0 h1:UtktXaU2Nb64z/pLiGIxY4431SJ4/dR5cjMmlVHgnT4=
|
||||
github.com/go-sql-driver/mysql v1.8.0/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
|
||||
github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM=
|
||||
github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
|
||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
|
||||
github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
|
||||
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/failsafe-go/failsafe-go v0.6.9 h1:7HWEzOlFOjNerxgWd8onWA2j/aEuqyAtuX6uWya/364=
|
||||
github.com/failsafe-go/failsafe-go v0.6.9/go.mod h1:zb7xfp1/DJ7Mn4xJhVSZ9F2qmmMEGvYHxEOHYK5SIm0=
|
||||
github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd h1:gbpYu9NMq8jhDVbvlGkMFWCjLFlqqEZjEmObmhUy6Vo=
|
||||
github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw=
|
||||
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/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
||||
github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4=
|
||||
github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY=
|
||||
github.com/invopop/yaml v0.2.0 h1:7zky/qH+O0DwAyoobXUqvVBwgBFRxKoQ/3FjcVpjTMY=
|
||||
github.com/invopop/yaml v0.2.0/go.mod h1:2XuRLgs/ouIrW3XNzuNj7J3Nvu/Dig5MXvbCEdiBN3Q=
|
||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
|
||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||
github.com/jackc/pgx/v5 v5.5.5 h1:amBjrZVmksIdNjxGW/IiIMzxMKZFelXbUoPNb+8sjQw=
|
||||
github.com/jackc/pgx/v5 v5.5.5/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A=
|
||||
github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
|
||||
github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||
github.com/joeshaw/multierror v0.0.0-20140124173710-69b34d4ec901 h1:rp+c0RAYOWj8l6qbCUTSiRLG/iKnW3K3/QfPPuSsBt4=
|
||||
github.com/joeshaw/multierror v0.0.0-20140124173710-69b34d4ec901/go.mod h1:Z86h9688Y0wesXCyonoVr47MasHilkuLMqGhRZ4Hpak=
|
||||
github.com/jonboulle/clockwork v0.4.0 h1:p4Cf1aMWXnXAUh8lVfewRBx1zaTSYKrKMF2g3ST4RZ4=
|
||||
github.com/jonboulle/clockwork v0.4.0/go.mod h1:xgRqUGwRcjKCO1vbZUEtSLrqKoPSsUpK7fnezOII0kc=
|
||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE=
|
||||
github.com/klauspost/compress v1.17.2 h1:RlWWUY/Dr4fL8qk9YG7DTZ7PDgME2V4csBXA8L/ixi4=
|
||||
github.com/klauspost/compress v1.17.2/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/labstack/echo/v4 v4.11.4 h1:vDZmA+qNeh1pd/cCkEicDMrjtrnMGQ1QFI9gWN1zGq8=
|
||||
github.com/labstack/echo/v4 v4.11.4/go.mod h1:noh7EvLwqDsmh/X/HWKPUl1AjzJrhyptRyEbQJfxen8=
|
||||
github.com/labstack/echo/v4 v4.12.0 h1:IKpw49IMryVB2p1a4dzwlhP1O2Tf2E0Ir/450lH+kI0=
|
||||
github.com/labstack/echo/v4 v4.12.0/go.mod h1:UP9Cr2DJXbOK3Kr9ONYzNowSh7HP0aG0ShAyycHSJvM=
|
||||
github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=
|
||||
github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU=
|
||||
github.com/libsql/sqlite-antlr4-parser v0.0.0-20230802215326-5cb5bb604475 h1:6PfEMwfInASh9hkN83aR0j4W/eKaAZt/AURtXAXlas0=
|
||||
github.com/libsql/sqlite-antlr4-parser v0.0.0-20230802215326-5cb5bb604475/go.mod h1:20nXSmcf0nAscrzqsXeC2/tA3KkV2eCiJqYuyAgl+ss=
|
||||
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
||||
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
|
||||
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/mfridman/interpolate v0.0.2 h1:pnuTK7MQIxxFz1Gr+rjSIx9u7qVjf5VOoM/u6BbAxPY=
|
||||
github.com/mfridman/interpolate v0.0.2/go.mod h1:p+7uk6oE07mpE/Ik1b8EckO0O4ZXiGAfshKBWLUM9Xg=
|
||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
|
||||
github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
|
||||
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw=
|
||||
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8=
|
||||
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
|
||||
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||
github.com/oapi-codegen/runtime v1.1.1 h1:EXLHh0DXIJnWhdRPN2w4MXAzFyE4CskzhNLUmtpMYro=
|
||||
github.com/oapi-codegen/runtime v1.1.1/go.mod h1:SK9X900oXmPWilYR5/WKPzt3Kqxn/uS/+lbpREv+eCg=
|
||||
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||
github.com/opencontainers/image-spec v1.1.0-rc5 h1:Ygwkfw9bpDvs+c9E34SdgGOj41dX/cbdlwvlWt0pnFI=
|
||||
github.com/opencontainers/image-spec v1.1.0-rc5/go.mod h1:X4pATf0uXsnn3g5aiGIsVnJBR4mxhKzfwmvK/B2NTm8=
|
||||
github.com/opencontainers/runc v1.1.12 h1:BOIssBaW1La0/qbNZHXOOa71dZfZEQOzW7dqQf3phss=
|
||||
github.com/opencontainers/runc v1.1.12/go.mod h1:S+lQwSfncpBha7XTy/5lBwWgm5+y5Ma/O44Ekby9FK8=
|
||||
github.com/ory/dockertest/v3 v3.10.0 h1:4K3z2VMe8Woe++invjaTB7VRyQXQy5UY+loujO4aNE4=
|
||||
github.com/ory/dockertest/v3 v3.10.0/go.mod h1:nr57ZbRWMqfsdGdFNLHz5jjNdDb7VVFnzAeW1n5N1Lg=
|
||||
github.com/paulmach/orb v0.10.0 h1:guVYVqzxHE/CQ1KpfGO077TR0ATHSNjp4s6XGLn3W9s=
|
||||
github.com/paulmach/orb v0.10.0/go.mod h1:5mULz1xQfs3bmQm63QEJA6lNGujuRafwA5S/EnuLaLU=
|
||||
github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s=
|
||||
github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw=
|
||||
github.com/pierrec/lz4/v4 v4.1.18 h1:xaKrnTkyoqfh1YItXl56+6KJNVYWlEEPuAQW9xsplYQ=
|
||||
github.com/pierrec/lz4/v4 v4.1.18/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pressly/goose/v3 v3.19.2 h1:z1yuD41jS4iaqLkyjkzGkKBz4rgyz/BYtCyMMGHlgzQ=
|
||||
github.com/pressly/goose/v3 v3.19.2/go.mod h1:BHkf3LzSBmO8E5FTMPupUYIpMTIh/ZuQVy+YTfhZLD4=
|
||||
github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
|
||||
github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
|
||||
github.com/pressly/goose/v3 v3.23.0 h1:57hqKos8izGek4v6D5+OXBa+Y4Rq8MU//+MmnevdpVA=
|
||||
github.com/pressly/goose/v3 v3.23.0/go.mod h1:rpx+D9GX/+stXmzKa+uh1DkjPnNVMdiOCV9iLdle4N8=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
|
||||
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
|
||||
github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys=
|
||||
github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
|
||||
github.com/sethvargo/go-retry v0.2.4 h1:T+jHEQy/zKJf5s95UkguisicE0zuF9y7+/vgz08Ocec=
|
||||
github.com/sethvargo/go-retry v0.2.4/go.mod h1:1afjQuvh7s4gflMObvjLPaWgluLLyhA1wmVZ6KLpICw=
|
||||
github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8=
|
||||
github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
|
||||
github.com/sethvargo/go-retry v0.3.0 h1:EEt31A35QhrcRZtrYFDTBg91cqZVnFL2navjDrah2SE=
|
||||
github.com/sethvargo/go-retry v0.3.0/go.mod h1:mNX17F0C/HguQMyMyJxcnU471gOZGxCLyYaFyAZraas=
|
||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0=
|
||||
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.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=
|
||||
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
|
||||
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
|
||||
github.com/vertica/vertica-sql-go v1.3.3 h1:fL+FKEAEy5ONmsvya2WH5T8bhkvY27y/Ik3ReR2T+Qw=
|
||||
github.com/vertica/vertica-sql-go v1.3.3/go.mod h1:jnn2GFuv+O2Jcjktb7zyc4Utlbu9YVqpHH/lx63+1M4=
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo=
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
|
||||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=
|
||||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
|
||||
github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=
|
||||
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
|
||||
github.com/ydb-platform/ydb-go-genproto v0.0.0-20240126124512-dbb0e1720dbf h1:ckwNHVo4bv2tqNkgx3W3HANh3ta1j6TR5qw08J1A7Tw=
|
||||
github.com/ydb-platform/ydb-go-genproto v0.0.0-20240126124512-dbb0e1720dbf/go.mod h1:Er+FePu1dNUieD+XTMDduGpQuCPssK5Q4BjF+IIXJ3I=
|
||||
github.com/ydb-platform/ydb-go-sdk/v3 v3.55.1 h1:Ebo6J5AMXgJ3A438ECYotA0aK7ETqjQx9WoZvVxzKBE=
|
||||
github.com/ydb-platform/ydb-go-sdk/v3 v3.55.1/go.mod h1:udNPW8eupyH/EZocecFmaSNJacKKYjzQa7cVgX5U2nc=
|
||||
go.opentelemetry.io/otel v1.20.0 h1:vsb/ggIY+hUjD/zCAQHpzTmndPqv/ml2ArbsbfBYTAc=
|
||||
go.opentelemetry.io/otel v1.20.0/go.mod h1:oUIGj3D77RwJdM6PPZImDpSZGDvkD9fhesHny69JFrs=
|
||||
go.opentelemetry.io/otel/trace v1.20.0 h1:+yxVAPZPbQhbC3OfAkeIVTky6iTFpcr4SiY9om7mXSQ=
|
||||
go.opentelemetry.io/otel/trace v1.20.0/go.mod h1:HJSK7F/hA5RlzpZ0zKDCHCDHm556LCDtKaAo6JmBFUU=
|
||||
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc=
|
||||
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
|
||||
golang.org/x/exp v0.0.0-20231108232855-2478ac86f678 h1:mchzmB1XO2pMaKFRqk/+MV3mgGG96aqaPXaMifQU47w=
|
||||
golang.org/x/exp v0.0.0-20231108232855-2478ac86f678/go.mod h1:zk2irFbV9DP96SEBUUAy67IdHUaZuSnrz1n472HUCLE=
|
||||
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.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw=
|
||||
golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U=
|
||||
golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
|
||||
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4=
|
||||
golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU=
|
||||
golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ=
|
||||
golang.org/x/sync v0.9.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=
|
||||
golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
|
||||
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/tools v0.17.0 h1:FvmRgNOcs3kOa+T20R1uhfP9F6HgG2mfxDv1vrx1Htc=
|
||||
golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20231106174013-bbf56f31fb17 h1:Jyp0Hsi0bmHXG6k9eATXoYtjd6e2UzZ1SCn/wIupY14=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20231106174013-bbf56f31fb17/go.mod h1:oQ5rr10WTTMvP4A36n8JpR1OrO1BEiV4f78CneXZxkA=
|
||||
google.golang.org/grpc v1.59.0 h1:Z5Iec2pjwb+LEOqzpB2MR12/eKFhDPhuqW91O+4bwUk=
|
||||
google.golang.org/grpc v1.59.0/go.mod h1:aUPDwccQo6OTjy7Hct4AfBPD1GptF4fyUjIkQ9YtF98=
|
||||
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
|
||||
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
|
||||
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
|
||||
golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg=
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142 h1:e7S5W7MGGLaSu8j3YjdezkZ+m1/Nm0uRVRMEMGk26Xs=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU=
|
||||
google.golang.org/grpc v1.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E=
|
||||
google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA=
|
||||
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
|
||||
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
howett.net/plist v1.0.0 h1:7CrbWYbPPO/PyNy38b2EB/+gYbjCe2DXBxgtOOZbSQM=
|
||||
howett.net/plist v1.0.0/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g=
|
||||
modernc.org/cc/v4 v4.21.4 h1:3Be/Rdo1fpr8GrQ7IVw9OHtplU4gWbb+wNgeoBMmGLQ=
|
||||
modernc.org/cc/v4 v4.21.4/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ=
|
||||
modernc.org/ccgo/v4 v4.19.2 h1:lwQZgvboKD0jBwdaeVCTouxhxAyN6iawF3STraAal8Y=
|
||||
modernc.org/ccgo/v4 v4.19.2/go.mod h1:ysS3mxiMV38XGRTTcgo0DQTeTmAO4oCmJl1nX9VFI3s=
|
||||
modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE=
|
||||
modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ=
|
||||
modernc.org/gc/v2 v2.4.1 h1:9cNzOqPyMJBvrUipmynX0ZohMhcxPtMccYgGOJdOiBw=
|
||||
modernc.org/gc/v2 v2.4.1/go.mod h1:wzN5dK1AzVGoH6XOzc3YZ+ey/jPgYHLuVckd62P0GYU=
|
||||
modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 h1:5D53IMaUuA5InSeMu9eJtlQXS2NxAhyWQvkKEgXZhHI=
|
||||
modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6/go.mod h1:Qz0X07sNOR1jWYCrJMEnbW/X55x206Q7Vt4mz6/wHp4=
|
||||
modernc.org/libc v1.41.0 h1:g9YAc6BkKlgORsUWj+JwqoB1wU3o4DE3bM3yvA3k+Gk=
|
||||
modernc.org/libc v1.41.0/go.mod h1:w0eszPsiXoOnoMJgrXjglgLuDy/bt5RR4y3QzUUeodY=
|
||||
modernc.org/libc v1.55.3 h1:AzcW1mhlPNrRtjS5sS+eW2ISCgSOLLNyFzRh/V3Qj/U=
|
||||
modernc.org/libc v1.55.3/go.mod h1:qFXepLhz+JjFThQ4kzwzOjA/y/artDeg+pcYnY+Q83w=
|
||||
modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=
|
||||
modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo=
|
||||
modernc.org/memory v1.7.2 h1:Klh90S215mmH8c9gO98QxQFsY+W451E8AnzjoE2ee1E=
|
||||
modernc.org/memory v1.7.2/go.mod h1:NO4NVCQy0N7ln+T9ngWqOQfi7ley4vpwvARR+Hjw95E=
|
||||
modernc.org/sqlite v1.29.5 h1:8l/SQKAjDtZFo9lkJLdk8g9JEOeYRG4/ghStDCCTiTE=
|
||||
modernc.org/sqlite v1.29.5/go.mod h1:S02dvcmm7TnTRvGhv8IGYyLnIt7AS2KPaB1F/71p75U=
|
||||
modernc.org/memory v1.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E=
|
||||
modernc.org/memory v1.8.0/go.mod h1:XPZ936zp5OMKGWPqbD3JShgd/ZoQ7899TUuQqxY+peU=
|
||||
modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4=
|
||||
modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
|
||||
modernc.org/sortutil v1.2.0 h1:jQiD3PfS2REGJNzNCMMaLSp/wdMNieTbKX920Cqdgqc=
|
||||
modernc.org/sortutil v1.2.0/go.mod h1:TKU2s7kJMf1AE84OoiGppNHJwvB753OYfNl2WRb++Ss=
|
||||
modernc.org/sqlite v1.34.1 h1:u3Yi6M0N8t9yKRDwhXcyp1eS5/ErhPTBggxWFuR6Hfk=
|
||||
modernc.org/sqlite v1.34.1/go.mod h1:pXV2xHxhzXZsgT/RtTFAPY6JJDEvOTcTdwADQCCWD4k=
|
||||
modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA=
|
||||
modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0=
|
||||
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
||||
nhooyr.io/websocket v1.8.7 h1:usjR2uOr/zjjkVMy0lW+PPohFok7PCow5sDjLgX4P4g=
|
||||
nhooyr.io/websocket v1.8.7/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0=
|
||||
|
|
127
httpserver/codes.go
Normal file
127
httpserver/codes.go
Normal file
|
@ -0,0 +1,127 @@
|
|||
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)
|
||||
}
|
||||
|
||||
logrus.WithFields(logrus.Fields{"lock": lock.ID, "code": code}).Debug("pushing code to lock")
|
||||
// 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()
|
||||
}
|
|
@ -10,35 +10,35 @@ import (
|
|||
"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
|
||||
ActiveCodes []db.IssuedCode
|
||||
Locks []db.Lock
|
||||
}
|
||||
|
||||
func (browserEndpoints) Index(c echo.Context) error {
|
||||
func indexHandler(c echo.Context) error {
|
||||
queries, dbc, err := db.Get()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
return frontend.Templates.ExecuteTemplate(c.Response(), "index.html", indexTemplateData{Locks: locks})
|
||||
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,
|
||||
})
|
||||
}
|
181
httpserver/lock.go
Normal file
181
httpserver/lock.go
Normal file
|
@ -0,0 +1,181 @@
|
|||
package httpserver
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"git.janky.solutions/finn/lockserver/db"
|
||||
"git.janky.solutions/finn/lockserver/zwavejs"
|
||||
echo "github.com/labstack/echo/v4"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func lockHandler(c echo.Context) error {
|
||||
lockID, err := strconv.ParseInt(c.Param("lock"), 10, 32)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid lock ID: %v", err)
|
||||
}
|
||||
|
||||
ctx := c.Request().Context()
|
||||
|
||||
queries, dbc, err := db.Get()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer dbc.Close()
|
||||
|
||||
lock, err := queries.GetLock(ctx, lockID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
codes, err := queries.GetAllLockCodesByLock(ctx, lockID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.Render(http.StatusFound, "lock.html", map[string]interface{}{
|
||||
"lock": lock,
|
||||
"codes": codes,
|
||||
})
|
||||
}
|
||||
|
||||
func lockEditHandler(c echo.Context) error {
|
||||
lockID, err := strconv.ParseInt(c.Param("lock"), 10, 32)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid lock ID: %v", err)
|
||||
}
|
||||
|
||||
ctx := c.Request().Context()
|
||||
|
||||
queries, dbc, err := db.Get()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer dbc.Close()
|
||||
|
||||
if c.Request().Method == http.MethodGet {
|
||||
lock, err := queries.GetLock(ctx, lockID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// return frontend.Templates.ExecuteTemplate(c.Response(), "lock-edit.html", lock)
|
||||
return c.Render(http.StatusFound, "lock-edit.html", lock)
|
||||
}
|
||||
|
||||
err = queries.UpdateLockName(ctx, db.UpdateLockNameParams{
|
||||
ID: lockID,
|
||||
Name: c.FormValue("name"),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
baseURL := c.Request().Header.Get("X-Ingress-Path")
|
||||
return c.Redirect(http.StatusFound, fmt.Sprintf("%s/locks/%d", baseURL, lockID))
|
||||
}
|
||||
|
||||
func lockCodeEditHandler(c echo.Context) error {
|
||||
lockID, err := strconv.ParseInt(c.Param("lock"), 10, 32)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid lock ID: %v", err)
|
||||
}
|
||||
|
||||
slot, err := strconv.ParseInt(c.Param("slot"), 10, 32)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid lock ID: %v", err)
|
||||
}
|
||||
|
||||
ctx := c.Request().Context()
|
||||
|
||||
queries, dbc, err := db.Get()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer dbc.Close()
|
||||
|
||||
code, err := queries.GetLockCodeBySlot(ctx, db.GetLockCodeBySlotParams{
|
||||
Lock: lockID,
|
||||
Slot: slot,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
lock, err := queries.GetLock(ctx, lockID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if c.Request().Method == http.MethodGet {
|
||||
logrus.WithField("lock", lockID).WithField("code", code.ID).Debug("querying logs")
|
||||
log, err := queries.GetLogForSlot(ctx, db.GetLogForSlotParams{
|
||||
Lock: lockID,
|
||||
Code: db.NullInt64(code.ID),
|
||||
})
|
||||
if err != nil && !errors.Is(err, sql.ErrNoRows) {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.Render(http.StatusFound, "lock-code-edit.html", map[string]interface{}{
|
||||
"lock": lock,
|
||||
"code": code,
|
||||
"log": log,
|
||||
})
|
||||
}
|
||||
|
||||
zwaveClient := c.Get(contextKeyZWaveClient).(*zwavejs.Client)
|
||||
|
||||
enabled := c.FormValue("enabled") == "on"
|
||||
enabledInt := 0
|
||||
if enabled {
|
||||
enabledInt = 1
|
||||
}
|
||||
err = zwaveClient.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.PropertyUserIDStatus)},
|
||||
PropertyName: zwavejs.AnyType{Type: zwavejs.AnyTypeString, String: string(zwavejs.PropertyUserIDStatus)},
|
||||
PropertyKey: zwavejs.AnyType{Type: zwavejs.AnyTypeInt, Int: int(slot)},
|
||||
}, zwavejs.AnyType{Type: zwavejs.AnyTypeInt, Int: enabledInt})
|
||||
if err != nil {
|
||||
return fmt.Errorf("error pushing enabled state to lock %s (ZWaveDeviceID=%d ID=%d): %v", lock.Name, lock.ZwaveDeviceID, lock.ID, err)
|
||||
}
|
||||
|
||||
newCode := "0000"
|
||||
if enabled {
|
||||
newCode = c.FormValue("code")
|
||||
err = zwaveClient.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)},
|
||||
}, zwavejs.AnyType{Type: zwavejs.AnyTypeString, String: newCode})
|
||||
if err != nil {
|
||||
return fmt.Errorf("error pushing code to lock %s (ZWaveDeviceID=%d ID=%d): %v", lock.Name, lock.ZwaveDeviceID, lock.ID, err)
|
||||
}
|
||||
}
|
||||
|
||||
err = queries.UpdateCodeSlot(ctx, db.UpdateCodeSlotParams{
|
||||
Lock: lockID,
|
||||
Slot: slot,
|
||||
Code: newCode,
|
||||
Name: c.FormValue("name"),
|
||||
Enabled: enabled,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
baseURL := c.Request().Header.Get("X-Ingress-Path")
|
||||
return c.Redirect(http.StatusFound, fmt.Sprintf("%s/locks/%d", baseURL, lockID))
|
||||
}
|
|
@ -11,26 +11,32 @@ import (
|
|||
"github.com/sirupsen/logrus"
|
||||
|
||||
"git.janky.solutions/finn/lockserver/config"
|
||||
"git.janky.solutions/finn/lockserver/openapi"
|
||||
"git.janky.solutions/finn/lockserver/frontend"
|
||||
"git.janky.solutions/finn/lockserver/zwavejs"
|
||||
)
|
||||
|
||||
const contextKeyZWaveClient = "zwave-client"
|
||||
|
||||
var server *echo.Echo
|
||||
|
||||
type lockserver struct {
|
||||
ZWaveJS *zwavejs.Client
|
||||
}
|
||||
|
||||
func ListenAndServe(client *zwavejs.Client) {
|
||||
func ListenAndServe(zwaveClient *zwavejs.Client) {
|
||||
server = echo.New()
|
||||
server.HideBanner = true
|
||||
server.HidePort = true
|
||||
server.HTTPErrorHandler = handleError
|
||||
server.Renderer = &Template{}
|
||||
server.Use(accessLogMiddleware)
|
||||
server.RouteNotFound("/*", tmpl("404.html"))
|
||||
|
||||
browserEndpoints{}.Register(server)
|
||||
|
||||
openapi.RegisterHandlersWithBaseURL(server, lockserver{ZWaveJS: client}, "/api")
|
||||
server.StaticFS("/static-"+config.Version, frontend.Static)
|
||||
server.GET("/", indexHandler)
|
||||
server.GET("/locks/:lock", lockHandler)
|
||||
server.GET("/locks/:lock/edit", lockEditHandler)
|
||||
server.POST("/locks/:lock/edit", lockEditHandler)
|
||||
server.GET("/locks/:lock/codes/:slot", lockCodeEditHandler)
|
||||
server.POST("/locks/:lock/codes/:slot", lockCodeEditHandler, addZWaveClientToContextKey(zwaveClient))
|
||||
server.GET("/add-code", tmpl("add-code.html"))
|
||||
server.POST("/add-code", addCode, addZWaveClientToContextKey(zwaveClient))
|
||||
|
||||
logrus.WithField("address", config.C.HTTPBind).Info("starting http server")
|
||||
err := server.Start(config.C.HTTPBind)
|
||||
|
@ -39,10 +45,18 @@ 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
|
||||
c.Response().WriteHeader(http.StatusNotFound)
|
||||
err = frontend.Templates.ExecuteTemplate(c.Response(), "404.html", getBaseTemplateData(c.Request().Header))
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
logrus.WithFields(logrus.Fields{
|
||||
|
@ -50,7 +64,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 {
|
||||
|
@ -76,8 +93,37 @@ func accessLogMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
|
|||
if err != nil {
|
||||
log = log.WithError(err)
|
||||
}
|
||||
log.Info("request handled")
|
||||
log.Debug("request handled")
|
||||
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,112 +0,0 @@
|
|||
package httpserver
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
echo "github.com/labstack/echo/v4"
|
||||
|
||||
"git.janky.solutions/finn/lockserver/db"
|
||||
"git.janky.solutions/finn/lockserver/openapi"
|
||||
"git.janky.solutions/finn/lockserver/zwavejs"
|
||||
)
|
||||
|
||||
func (lockserver) GetLockCodeSlot(c echo.Context, lock int, slot int) error {
|
||||
queries, dbc, err := db.Get()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer dbc.Close()
|
||||
|
||||
slotData, err := queries.GetLockCodeBySlot(c.Request().Context(), db.GetLockCodeBySlotParams{
|
||||
Lock: int64(lock),
|
||||
Slot: int64(slot),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusFound, slotData.OpenAPI())
|
||||
}
|
||||
|
||||
func (l lockserver) PutLockCodeSlot(c echo.Context, lockID int, slot int) error {
|
||||
queries, dbc, err := db.Get()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer dbc.Close()
|
||||
|
||||
var body openapi.LockCodeSlot
|
||||
if err := c.Bind(&body); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ctx := c.Request().Context()
|
||||
|
||||
lock, err := queries.GetLock(ctx, int64(lockID))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 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 = l.ZWaveJS.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: slot},
|
||||
}, zwavejs.AnyType{Type: zwavejs.AnyTypeString, String: body.Code})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
enabled := 0
|
||||
if body.Enabled {
|
||||
enabled = 1
|
||||
}
|
||||
err = l.ZWaveJS.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.PropertyUserIDStatus)},
|
||||
PropertyName: zwavejs.AnyType{Type: zwavejs.AnyTypeString, String: string(zwavejs.PropertyUserIDStatus)},
|
||||
PropertyKey: zwavejs.AnyType{Type: zwavejs.AnyTypeInt, Int: slot},
|
||||
}, zwavejs.AnyType{Type: zwavejs.AnyTypeInt, Int: enabled})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = queries.UpsertCodeSlot(ctx, db.UpsertCodeSlotParams{
|
||||
Lock: lock.ID,
|
||||
Slot: int64(slot),
|
||||
Code: body.Code,
|
||||
Enabled: body.Enabled,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, body)
|
||||
}
|
33
httpserver/templates.go
Normal file
33
httpserver/templates.go
Normal file
|
@ -0,0 +1,33 @@
|
|||
package httpserver
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
||||
"git.janky.solutions/finn/lockserver/frontend"
|
||||
echo "github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
type Template struct {
|
||||
}
|
||||
|
||||
type templateData struct {
|
||||
BaseURL string
|
||||
Username string
|
||||
UserDisplayName string
|
||||
Data any
|
||||
}
|
||||
|
||||
func buildTemplateData(c echo.Context, data any) templateData {
|
||||
headers := c.Request().Header
|
||||
|
||||
return templateData{
|
||||
BaseURL: headers.Get("X-Ingress-Path"),
|
||||
Username: headers.Get("X-Remote-User-Name"),
|
||||
UserDisplayName: headers.Get("X-Remote-User-Display-Name"),
|
||||
Data: data,
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Template) Render(w io.Writer, name string, data any, c echo.Context) error {
|
||||
return frontend.Templates.ExecuteTemplate(c.Response(), name, buildTemplateData(c, data))
|
||||
}
|
|
@ -1,51 +0,0 @@
|
|||
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)
|
||||
}
|
|
@ -1,6 +1,5 @@
|
|||
{
|
||||
"zwave-js-server": "ws://home-assistant:3000",
|
||||
"sqlite-database": "lockserver.db",
|
||||
"http-bind": ":8080",
|
||||
"session-secrets": []
|
||||
"sqlite-database": "/data/lockserver.db",
|
||||
"http-bind": ":8080"
|
||||
}
|
||||
|
|
|
@ -1,9 +0,0 @@
|
|||
package: openapi
|
||||
generate:
|
||||
models: true
|
||||
embedded-spec: true
|
||||
strict-server: true
|
||||
echo-server: true
|
||||
output-options:
|
||||
skip-prune: true
|
||||
output: openapi/openapi.go
|
|
@ -1,454 +0,0 @@
|
|||
// Package openapi provides primitives to interact with the openapi HTTP API.
|
||||
//
|
||||
// Code generated by github.com/deepmap/oapi-codegen/v2 version v2.1.0 DO NOT EDIT.
|
||||
package openapi
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"github.com/getkin/kin-openapi/openapi3"
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/oapi-codegen/runtime"
|
||||
strictecho "github.com/oapi-codegen/runtime/strictmiddleware/echo"
|
||||
)
|
||||
|
||||
// LockCodeSlot defines model for LockCodeSlot.
|
||||
type LockCodeSlot struct {
|
||||
Code string `json:"code"`
|
||||
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
|
||||
|
||||
// (PUT /locks/{lock}/slots/{slot})
|
||||
PutLockCodeSlot(ctx echo.Context, lock int, slot int) error
|
||||
}
|
||||
|
||||
// ServerInterfaceWrapper converts echo contexts to parameters.
|
||||
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
|
||||
// ------------- Path parameter "lock" -------------
|
||||
var lock int
|
||||
|
||||
err = runtime.BindStyledParameterWithOptions("simple", "lock", ctx.Param("lock"), &lock, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true})
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter lock: %s", err))
|
||||
}
|
||||
|
||||
// ------------- Path parameter "slot" -------------
|
||||
var slot int
|
||||
|
||||
err = runtime.BindStyledParameterWithOptions("simple", "slot", ctx.Param("slot"), &slot, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true})
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter slot: %s", err))
|
||||
}
|
||||
|
||||
// Invoke the callback with all the unmarshaled arguments
|
||||
err = w.Handler.GetLockCodeSlot(ctx, lock, slot)
|
||||
return err
|
||||
}
|
||||
|
||||
// PutLockCodeSlot converts echo context to params.
|
||||
func (w *ServerInterfaceWrapper) PutLockCodeSlot(ctx echo.Context) error {
|
||||
var err error
|
||||
// ------------- Path parameter "lock" -------------
|
||||
var lock int
|
||||
|
||||
err = runtime.BindStyledParameterWithOptions("simple", "lock", ctx.Param("lock"), &lock, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true})
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter lock: %s", err))
|
||||
}
|
||||
|
||||
// ------------- Path parameter "slot" -------------
|
||||
var slot int
|
||||
|
||||
err = runtime.BindStyledParameterWithOptions("simple", "slot", ctx.Param("slot"), &slot, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true})
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter slot: %s", err))
|
||||
}
|
||||
|
||||
// Invoke the callback with all the unmarshaled arguments
|
||||
err = w.Handler.PutLockCodeSlot(ctx, lock, slot)
|
||||
return err
|
||||
}
|
||||
|
||||
// This is a simple interface which specifies echo.Route addition functions which
|
||||
// are present on both echo.Echo and echo.Group, since we want to allow using
|
||||
// either of them for path registration
|
||||
type EchoRouter interface {
|
||||
CONNECT(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route
|
||||
DELETE(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route
|
||||
GET(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route
|
||||
HEAD(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route
|
||||
OPTIONS(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route
|
||||
PATCH(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route
|
||||
POST(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route
|
||||
PUT(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route
|
||||
TRACE(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route
|
||||
}
|
||||
|
||||
// RegisterHandlers adds each server route to the EchoRouter.
|
||||
func RegisterHandlers(router EchoRouter, si ServerInterface) {
|
||||
RegisterHandlersWithBaseURL(router, si, "")
|
||||
}
|
||||
|
||||
// Registers handlers, and prepends BaseURL to the paths, so that the paths
|
||||
// can be served under a prefix.
|
||||
func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL string) {
|
||||
|
||||
wrapper := ServerInterfaceWrapper{
|
||||
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"`
|
||||
}
|
||||
|
||||
type GetLockCodeSlotResponseObject interface {
|
||||
VisitGetLockCodeSlotResponse(w http.ResponseWriter) error
|
||||
}
|
||||
|
||||
type GetLockCodeSlot200JSONResponse LockCodeSlot
|
||||
|
||||
func (response GetLockCodeSlot200JSONResponse) VisitGetLockCodeSlotResponse(w http.ResponseWriter) error {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(200)
|
||||
|
||||
return json.NewEncoder(w).Encode(response)
|
||||
}
|
||||
|
||||
type PutLockCodeSlotRequestObject struct {
|
||||
Lock int `json:"lock"`
|
||||
Slot int `json:"slot"`
|
||||
Body *PutLockCodeSlotJSONRequestBody
|
||||
}
|
||||
|
||||
type PutLockCodeSlotResponseObject interface {
|
||||
VisitPutLockCodeSlotResponse(w http.ResponseWriter) error
|
||||
}
|
||||
|
||||
// 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)
|
||||
|
||||
// (PUT /locks/{lock}/slots/{slot})
|
||||
PutLockCodeSlot(ctx context.Context, request PutLockCodeSlotRequestObject) (PutLockCodeSlotResponseObject, error)
|
||||
}
|
||||
|
||||
type StrictHandlerFunc = strictecho.StrictEchoHandlerFunc
|
||||
type StrictMiddlewareFunc = strictecho.StrictEchoMiddlewareFunc
|
||||
|
||||
func NewStrictHandler(ssi StrictServerInterface, middlewares []StrictMiddlewareFunc) ServerInterface {
|
||||
return &strictHandler{ssi: ssi, middlewares: middlewares}
|
||||
}
|
||||
|
||||
type strictHandler struct {
|
||||
ssi StrictServerInterface
|
||||
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
|
||||
|
||||
request.Lock = lock
|
||||
request.Slot = slot
|
||||
|
||||
handler := func(ctx echo.Context, request interface{}) (interface{}, error) {
|
||||
return sh.ssi.GetLockCodeSlot(ctx.Request().Context(), request.(GetLockCodeSlotRequestObject))
|
||||
}
|
||||
for _, middleware := range sh.middlewares {
|
||||
handler = middleware(handler, "GetLockCodeSlot")
|
||||
}
|
||||
|
||||
response, err := handler(ctx, request)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
} else if validResponse, ok := response.(GetLockCodeSlotResponseObject); ok {
|
||||
return validResponse.VisitGetLockCodeSlotResponse(ctx.Response())
|
||||
} else if response != nil {
|
||||
return fmt.Errorf("unexpected response type: %T", response)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// PutLockCodeSlot operation middleware
|
||||
func (sh *strictHandler) PutLockCodeSlot(ctx echo.Context, lock int, slot int) error {
|
||||
var request PutLockCodeSlotRequestObject
|
||||
|
||||
request.Lock = lock
|
||||
request.Slot = slot
|
||||
|
||||
var body PutLockCodeSlotJSONRequestBody
|
||||
if err := ctx.Bind(&body); err != nil {
|
||||
return err
|
||||
}
|
||||
request.Body = &body
|
||||
|
||||
handler := func(ctx echo.Context, request interface{}) (interface{}, error) {
|
||||
return sh.ssi.PutLockCodeSlot(ctx.Request().Context(), request.(PutLockCodeSlotRequestObject))
|
||||
}
|
||||
for _, middleware := range sh.middlewares {
|
||||
handler = middleware(handler, "PutLockCodeSlot")
|
||||
}
|
||||
|
||||
response, err := handler(ctx, request)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
} else if validResponse, ok := response.(PutLockCodeSlotResponseObject); ok {
|
||||
return validResponse.VisitPutLockCodeSlotResponse(ctx.Response())
|
||||
} else if response != nil {
|
||||
return fmt.Errorf("unexpected response type: %T", response)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Base64 encoded, gzipped, json marshaled Swagger object
|
||||
var swaggerSpec = []string{
|
||||
|
||||
"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
|
||||
// or error if failed to decode
|
||||
func decodeSpec() ([]byte, error) {
|
||||
zipped, err := base64.StdEncoding.DecodeString(strings.Join(swaggerSpec, ""))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error base64 decoding spec: %w", err)
|
||||
}
|
||||
zr, err := gzip.NewReader(bytes.NewReader(zipped))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error decompressing spec: %w", err)
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
_, err = buf.ReadFrom(zr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error decompressing spec: %w", err)
|
||||
}
|
||||
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
var rawSpec = decodeSpecCached()
|
||||
|
||||
// a naive cached of a decoded swagger spec
|
||||
func decodeSpecCached() func() ([]byte, error) {
|
||||
data, err := decodeSpec()
|
||||
return func() ([]byte, error) {
|
||||
return data, err
|
||||
}
|
||||
}
|
||||
|
||||
// Constructs a synthetic filesystem for resolving external references when loading openapi specifications.
|
||||
func PathToRawSpec(pathToFile string) map[string]func() ([]byte, error) {
|
||||
res := make(map[string]func() ([]byte, error))
|
||||
if len(pathToFile) > 0 {
|
||||
res[pathToFile] = rawSpec
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
// GetSwagger returns the Swagger specification corresponding to the generated code
|
||||
// in this file. The external references of Swagger specification are resolved.
|
||||
// The logic of resolving external references is tightly connected to "import-mapping" feature.
|
||||
// Externally referenced files must be embedded in the corresponding golang packages.
|
||||
// Urls can be supported but this task was out of the scope.
|
||||
func GetSwagger() (swagger *openapi3.T, err error) {
|
||||
resolvePath := PathToRawSpec("")
|
||||
|
||||
loader := openapi3.NewLoader()
|
||||
loader.IsExternalRefsAllowed = true
|
||||
loader.ReadFromURIFunc = func(loader *openapi3.Loader, url *url.URL) ([]byte, error) {
|
||||
pathToFile := url.String()
|
||||
pathToFile = path.Clean(pathToFile)
|
||||
getSpec, ok := resolvePath[pathToFile]
|
||||
if !ok {
|
||||
err1 := fmt.Errorf("path not found: %s", pathToFile)
|
||||
return nil, err1
|
||||
}
|
||||
return getSpec()
|
||||
}
|
||||
var specData []byte
|
||||
specData, err = rawSpec()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
swagger, err = loader.LoadFromData(specData)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
|
@ -1,104 +0,0 @@
|
|||
openapi: "3.1.0"
|
||||
info:
|
||||
version: 0.1.0
|
||||
title: Lockserver
|
||||
license:
|
||||
name: CC-BY-SA-4.0
|
||||
url: https://creativecommons.org/licenses/by/4.0/
|
||||
paths:
|
||||
/locks/{lock}/slots/{slot}:
|
||||
get:
|
||||
operationId: get_lock_code_slot
|
||||
parameters:
|
||||
- name: lock
|
||||
in: path
|
||||
description: ID of lock
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
- name: slot
|
||||
in: path
|
||||
description: ID of code slot
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
responses:
|
||||
'200':
|
||||
description: A lock code slot
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/LockCodeSlot'
|
||||
put:
|
||||
operationId: put_lock_code_slot
|
||||
parameters:
|
||||
- name: lock
|
||||
in: path
|
||||
description: ID of lock
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
- name: slot
|
||||
in: path
|
||||
description: ID of code slot
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
requestBody:
|
||||
description: the new value for the slot
|
||||
required: true
|
||||
content:
|
||||
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:
|
||||
type: object
|
||||
required:
|
||||
- code
|
||||
- enabled
|
||||
properties:
|
||||
code:
|
||||
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
|
3
renovate.json
Normal file
3
renovate.json
Normal file
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json"
|
||||
}
|
|
@ -43,8 +43,10 @@ func (c *Client) DialAndListen(ctx context.Context) {
|
|||
|
||||
for {
|
||||
logrus.WithField("server", c.Server).Info("connecting to zwave-js server")
|
||||
ctxWithTimeout, cancel := context.WithTimeout(ctx, time.Second*5)
|
||||
defer cancel()
|
||||
conn, err := failsafe.Get(func() (*websocket.Conn, error) {
|
||||
conn, _, err := websocket.DefaultDialer.DialContext(ctx, c.Server, nil)
|
||||
conn, _, err := websocket.DefaultDialer.DialContext(ctxWithTimeout, c.Server, nil)
|
||||
return conn, err
|
||||
}, connectRetryPolicy)
|
||||
if err != nil {
|
||||
|
@ -59,9 +61,11 @@ func (c *Client) DialAndListen(ctx context.Context) {
|
|||
}
|
||||
|
||||
logrus.WithError(err).Error("error communicating with zwavejs server")
|
||||
time.Sleep(time.Second * 5)
|
||||
continue
|
||||
}
|
||||
_ = c.conn.Close()
|
||||
cancel()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -93,7 +97,7 @@ func (c *Client) listen(ctx context.Context) error {
|
|||
case "result":
|
||||
if msg.MessageID == "" {
|
||||
if err := syncState(ctx, *msg.Result); err != nil {
|
||||
return err
|
||||
return fmt.Errorf("error syncing state from zwavejs server: %v", err)
|
||||
}
|
||||
} else {
|
||||
c.handleCallback(msg.MessageID, *msg.Result)
|
||||
|
@ -233,6 +237,8 @@ func (c *Client) handleCallback(messageID string, result Result) error {
|
|||
c.callbacksLock.Lock()
|
||||
defer c.callbacksLock.Unlock()
|
||||
|
||||
logrus.WithField("message_id", messageID).Debug(fmt.Sprintf("%+v", result))
|
||||
|
||||
cb, ok := c.callbacks[messageID]
|
||||
if !ok {
|
||||
logrus.WithField("message_id", messageID).Warn("got response to a message we didn't send")
|
||||
|
@ -254,6 +260,10 @@ func (c *Client) sendMessage(message OutgoingMessageIface) (Result, error) {
|
|||
c.callbacks[messageID] = ch
|
||||
c.callbacksLock.Unlock()
|
||||
|
||||
// w := logrus.WithField("message_id", messageID).Writer()
|
||||
// json.NewEncoder(w).Encode(message)
|
||||
// w.Close()
|
||||
|
||||
if err := c.conn.WriteJSON(message); err != nil {
|
||||
return Result{}, err
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ import (
|
|||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
@ -64,3 +65,84 @@ func (n AnyType) MarshalJSON() ([]byte, error) {
|
|||
return nil, errors.New("anytype of unknown type")
|
||||
}
|
||||
}
|
||||
|
||||
type NodeValueTypeType int
|
||||
|
||||
const (
|
||||
NodeValueTypeString NodeValueTypeType = iota
|
||||
NodeValueTypeInt
|
||||
NodeValueTypeBool
|
||||
NodeValueTypeList
|
||||
NodeValueTypeIntWithUnit
|
||||
)
|
||||
|
||||
type NodeValueType struct {
|
||||
Type NodeValueTypeType
|
||||
String string
|
||||
Int int
|
||||
Bool bool
|
||||
List []NodeValueType
|
||||
}
|
||||
|
||||
func (n *NodeValueType) UnmarshalJSON(data []byte) error {
|
||||
if bytes.HasPrefix(data, []byte("\"")) {
|
||||
n.Type = NodeValueTypeString
|
||||
return json.Unmarshal(data, &n.String)
|
||||
}
|
||||
|
||||
if bytes.Equal(data, []byte("true")) || bytes.Equal(data, []byte("false")) {
|
||||
n.Type = NodeValueTypeBool
|
||||
return json.Unmarshal(data, &n.Bool)
|
||||
}
|
||||
|
||||
if bytes.HasPrefix(data, []byte("[")) {
|
||||
n.Type = NodeValueTypeList
|
||||
return json.Unmarshal(data, &n.List)
|
||||
}
|
||||
|
||||
if bytes.HasPrefix(data, []byte("{")) {
|
||||
var value map[string]interface{}
|
||||
if err := json.Unmarshal(data, &value); err != nil {
|
||||
return fmt.Errorf("error unmarshalling NodeValueType object %s: %v", string(data), err)
|
||||
}
|
||||
|
||||
if intval, ok := value["value"].(int); ok {
|
||||
n.Int = intval
|
||||
}
|
||||
|
||||
if unit, ok := value["unit"].(string); ok {
|
||||
n.String = unit
|
||||
}
|
||||
|
||||
n.Type = NodeValueTypeIntWithUnit
|
||||
return nil
|
||||
}
|
||||
|
||||
n.Type = NodeValueTypeInt
|
||||
if err := json.Unmarshal(data, &n.Int); err != nil {
|
||||
logrus.WithField("value", string(data)).Debug("error while parsing node property value of ambiguous type")
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n NodeValueType) MarshalJSON() ([]byte, error) {
|
||||
switch n.Type {
|
||||
case NodeValueTypeString:
|
||||
return json.Marshal(n.String)
|
||||
case NodeValueTypeBool:
|
||||
return json.Marshal(n.Bool)
|
||||
case NodeValueTypeList:
|
||||
return json.Marshal(n.List)
|
||||
case NodeValueTypeInt:
|
||||
return json.Marshal(n.Int)
|
||||
case NodeValueTypeIntWithUnit:
|
||||
return json.Marshal(map[string]interface{}{
|
||||
"value": n.Int,
|
||||
"unit": n.String,
|
||||
})
|
||||
default:
|
||||
return nil, errors.New("NodeValueType of unknown type")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@ type IncomingMessage struct {
|
|||
// Values for type = version
|
||||
DriverVersion string `json:"driverVersion"`
|
||||
ServerVersion string `json:"serverVersion"`
|
||||
HomeID int `json:"homeId"`
|
||||
HomeID int64 `json:"homeId"`
|
||||
|
||||
Event *Event
|
||||
Result *Result
|
||||
|
@ -181,7 +181,7 @@ type NodeValue struct {
|
|||
PropertyKey AnyType `json:"propertyKey"`
|
||||
CCVersion int `json:"ccVersion"`
|
||||
Metadata NodeValuesMetadata `json:"metadata"`
|
||||
Value AnyType `json:"value"`
|
||||
Value NodeValueType `json:"value"`
|
||||
}
|
||||
|
||||
type NodeValuesMetadata struct {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue