some user rest crud, refactor middleware, start admin
This commit is contained in:
parent
8e844748c9
commit
bf7e3d7785
21
.editorconfig
Normal file
21
.editorconfig
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
# EditorConfig is awesome: http://EditorConfig.org
|
||||||
|
|
||||||
|
# top-most EditorConfig file
|
||||||
|
root = true
|
||||||
|
|
||||||
|
# Unix-style newlines with a newline ending every file
|
||||||
|
[*]
|
||||||
|
end_of_line = lf
|
||||||
|
insert_final_newline = true
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
max_line_length = 120
|
||||||
|
|
||||||
|
[*.go]
|
||||||
|
end_of_line = lf
|
||||||
|
insert_final_newline = true
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
indent_style = tab
|
||||||
|
indent_size = 4
|
||||||
|
max_line_length = 120
|
@ -71,6 +71,10 @@ var userAddCommand = &command.Command{
|
|||||||
Description: "a custom greeting for the user",
|
Description: "a custom greeting for the user",
|
||||||
Default: "",
|
Default: "",
|
||||||
},
|
},
|
||||||
|
"admin": {
|
||||||
|
Type: "bool",
|
||||||
|
Description: "make this user an admin",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
Action: func(cmd *command.Command) error {
|
Action: func(cmd *command.Command) error {
|
||||||
config := cmd.Options["config"].ToValue().(string)
|
config := cmd.Options["config"].ToValue().(string)
|
||||||
@ -80,6 +84,7 @@ var userAddCommand = &command.Command{
|
|||||||
schedule := cmd.Options["schedule"].ToString()
|
schedule := cmd.Options["schedule"].ToString()
|
||||||
ttl := cmd.Options["ttl"].ToString()
|
ttl := cmd.Options["ttl"].ToString()
|
||||||
greeting := cmd.Options["greeting"].ToString()
|
greeting := cmd.Options["greeting"].ToString()
|
||||||
|
admin := cmd.Options["admin"].ToValue().(bool)
|
||||||
|
|
||||||
data, err := os.ReadFile(config)
|
data, err := os.ReadFile(config)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -105,19 +110,16 @@ var userAddCommand = &command.Command{
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
var ttlDuration auth.Duration
|
|
||||||
if err := ttlDuration.UnmarshalDB(ttl); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
user := &auth.User{
|
user := &auth.User{
|
||||||
Name: cmd.Arguments[0].ToString(),
|
Name: cmd.Arguments[0].ToString(),
|
||||||
Password: string(password),
|
Password: string(password),
|
||||||
Handle: cmd.Arguments[1].ToString(),
|
Handle: cmd.Arguments[1].ToString(),
|
||||||
Greeting: greeting,
|
Greeting: greeting,
|
||||||
TTL: ttlDuration,
|
IsAdmin: admin,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
*user.TTL = auth.TTL(ttl)
|
||||||
|
|
||||||
if schedule != "" {
|
if schedule != "" {
|
||||||
user.Schedule = &auth.UserSchedule{}
|
user.Schedule = &auth.UserSchedule{}
|
||||||
if err := user.Schedule.UnmarshalDB([]byte(schedule)); err != nil {
|
if err := user.Schedule.UnmarshalDB([]byte(schedule)); err != nil {
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
package hue
|
package hue
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"git.rob.mx/nidito/chinampa"
|
"git.rob.mx/nidito/chinampa"
|
||||||
@ -37,13 +38,18 @@ var setupHueCommand = &command.Command{
|
|||||||
domain := cmd.Arguments[1].ToValue().(string)
|
domain := cmd.Arguments[1].ToValue().(string)
|
||||||
|
|
||||||
logrus.Infof("Setting up with bridge at %s, app %s", ip, domain)
|
logrus.Infof("Setting up with bridge at %s, app %s", ip, domain)
|
||||||
d := door.NewHue(map[string]any{
|
doorI, err := door.NewHue(map[string]any{
|
||||||
"ip": ip,
|
"ip": ip,
|
||||||
"username": "",
|
"username": "",
|
||||||
"device": -1,
|
"device": -1,
|
||||||
}).(*door.Hue)
|
})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("could not connect to door: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
return d.Setup(os.Args[2])
|
adapter := doorI.(*door.Hue)
|
||||||
|
|
||||||
|
return adapter.Setup(os.Args[2])
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -74,12 +80,16 @@ var testHueCommand = &command.Command{
|
|||||||
device := cmd.Arguments[2].ToValue().(string)
|
device := cmd.Arguments[2].ToValue().(string)
|
||||||
|
|
||||||
logrus.Infof("Testing bridge at %s, username %s, device %s", ip, username, device)
|
logrus.Infof("Testing bridge at %s, username %s, device %s", ip, username, device)
|
||||||
d := door.NewHue(map[string]any{
|
|
||||||
|
err := door.Connect(map[string]any{
|
||||||
|
"adapter": "hue",
|
||||||
"ip": ip,
|
"ip": ip,
|
||||||
"username": username,
|
"username": username,
|
||||||
"device": device,
|
"device": device,
|
||||||
})
|
})
|
||||||
|
if err != nil {
|
||||||
return door.RequestToEnter(d, "test")
|
return fmt.Errorf("could not connect to door: %s", err)
|
||||||
|
}
|
||||||
|
return door.RequestToEnter("test")
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
2
go.mod
2
go.mod
@ -3,7 +3,7 @@ module git.rob.mx/nidito/puerta
|
|||||||
go 1.18
|
go 1.18
|
||||||
|
|
||||||
require (
|
require (
|
||||||
git.rob.mx/nidito/chinampa v0.0.0-20230101000754-0d24a8007547
|
git.rob.mx/nidito/chinampa v0.0.0-20230102065449-d9b257e145ce
|
||||||
github.com/alexedwards/scs/v2 v2.5.0
|
github.com/alexedwards/scs/v2 v2.5.0
|
||||||
github.com/amimof/huego v1.2.1
|
github.com/amimof/huego v1.2.1
|
||||||
github.com/go-webauthn/webauthn v0.6.0
|
github.com/go-webauthn/webauthn v0.6.0
|
||||||
|
17
go.sum
17
go.sum
@ -1,12 +1,6 @@
|
|||||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||||
git.rob.mx/nidito/chinampa v0.0.0-20221229190558-4eec8e55a1e6 h1:yTb1uOLMMGKGcA1fr1XHJyRcKb6OXhgvcLDLzr/lua0=
|
git.rob.mx/nidito/chinampa v0.0.0-20230102065449-d9b257e145ce h1:fKG3wUdPgsviY2mE79vhXL4CalNdvhkL6vDAtdyVt0I=
|
||||||
git.rob.mx/nidito/chinampa v0.0.0-20221229190558-4eec8e55a1e6/go.mod h1:nQlQqIQ6UuP6spFFZvfVT1MhQJYEA7B3Y2EtM2Fha3Y=
|
git.rob.mx/nidito/chinampa v0.0.0-20230102065449-d9b257e145ce/go.mod h1:obhWsLkUIlKJyhfa7uunrSs2O44JBqsegSAtAvY2LRM=
|
||||||
git.rob.mx/nidito/chinampa v0.0.0-20221230070026-831e68c7b70a h1:kvoP8pEeIr4aSK00IiKoANMxM3r4aXnvJOmLRBbglG0=
|
|
||||||
git.rob.mx/nidito/chinampa v0.0.0-20221230070026-831e68c7b70a/go.mod h1:nQlQqIQ6UuP6spFFZvfVT1MhQJYEA7B3Y2EtM2Fha3Y=
|
|
||||||
git.rob.mx/nidito/chinampa v0.0.0-20221231055324-8ea5f42ef848 h1:Nvyo7qK6oVLWQ2aHRtQ5AAMcVEue51Wr+hxBF4OzMkE=
|
|
||||||
git.rob.mx/nidito/chinampa v0.0.0-20221231055324-8ea5f42ef848/go.mod h1:jZwWmhBRfjJjp2jwM/+jIGgfWLQPudgAah+wKCKjBfk=
|
|
||||||
git.rob.mx/nidito/chinampa v0.0.0-20230101000754-0d24a8007547 h1:9a4zHaJbeTqcPnF0WKNa+Kgzm+ZD3f887ZZTRWDPh3M=
|
|
||||||
git.rob.mx/nidito/chinampa v0.0.0-20230101000754-0d24a8007547/go.mod h1:obhWsLkUIlKJyhfa7uunrSs2O44JBqsegSAtAvY2LRM=
|
|
||||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||||
github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
|
github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
|
||||||
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||||
@ -19,7 +13,6 @@ github.com/alexedwards/scs/v2 v2.5.0/go.mod h1:ToaROZxyKukJKT/xLcVQAChi5k6+Pn1Gv
|
|||||||
github.com/amimof/huego v1.2.1 h1:kd36vsieclW4fZ4Vqii9DNU2+6ptWWtkp4OG0AXM8HE=
|
github.com/amimof/huego v1.2.1 h1:kd36vsieclW4fZ4Vqii9DNU2+6ptWWtkp4OG0AXM8HE=
|
||||||
github.com/amimof/huego v1.2.1/go.mod h1:z1Sy7Rrdzmb+XsGHVEhODrRJRDq4RCFW7trCI5cKmeA=
|
github.com/amimof/huego v1.2.1/go.mod h1:z1Sy7Rrdzmb+XsGHVEhODrRJRDq4RCFW7trCI5cKmeA=
|
||||||
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
|
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
|
||||||
github.com/aymanbagabas/go-osc52 v1.0.3 h1:DTwqENW7X9arYimJrPeGZcV0ln14sGMt3pHZspWD+Mg=
|
|
||||||
github.com/aymanbagabas/go-osc52 v1.0.3/go.mod h1:zT8H+Rk4VSabYN90pWyugflM3ZhpTZNC7cASDfUCdT4=
|
github.com/aymanbagabas/go-osc52 v1.0.3/go.mod h1:zT8H+Rk4VSabYN90pWyugflM3ZhpTZNC7cASDfUCdT4=
|
||||||
github.com/aymanbagabas/go-osc52 v1.2.1 h1:q2sWUyDcozPLcLabEMd+a+7Ea2DitxZVN9hTxab9L4E=
|
github.com/aymanbagabas/go-osc52 v1.2.1 h1:q2sWUyDcozPLcLabEMd+a+7Ea2DitxZVN9hTxab9L4E=
|
||||||
github.com/aymanbagabas/go-osc52 v1.2.1/go.mod h1:zT8H+Rk4VSabYN90pWyugflM3ZhpTZNC7cASDfUCdT4=
|
github.com/aymanbagabas/go-osc52 v1.2.1/go.mod h1:zT8H+Rk4VSabYN90pWyugflM3ZhpTZNC7cASDfUCdT4=
|
||||||
@ -51,7 +44,6 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
|
|||||||
github.com/denisenkom/go-mssqldb v0.11.0/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
|
github.com/denisenkom/go-mssqldb v0.11.0/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
|
||||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
||||||
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
|
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
|
||||||
github.com/dlclark/regexp2 v1.4.0 h1:F1rxgk7p4uKjwIQxBs9oAXe5CqrXlCduYEJvrF4u93E=
|
|
||||||
github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
|
github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
|
||||||
github.com/dlclark/regexp2 v1.7.0 h1:7lJfhqlPssTb1WQx4yvTHN0uElPEv52sbaECrAQxjAo=
|
github.com/dlclark/regexp2 v1.7.0 h1:7lJfhqlPssTb1WQx4yvTHN0uElPEv52sbaECrAQxjAo=
|
||||||
github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||||
@ -126,7 +118,6 @@ github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgf
|
|||||||
github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
|
github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
|
||||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||||
github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc=
|
|
||||||
github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||||
@ -254,7 +245,6 @@ github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40T
|
|||||||
github.com/remyoudompheng/bigfft v0.0.0-20190728182440-6a916e37a237/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
github.com/remyoudompheng/bigfft v0.0.0-20190728182440-6a916e37a237/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||||
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||||
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
|
||||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||||
github.com/rivo/uniseg v0.4.3 h1:utMvzDsuh3suAEnhH0RdHmoPbU648o6CvXxTx4SBMOw=
|
github.com/rivo/uniseg v0.4.3 h1:utMvzDsuh3suAEnhH0RdHmoPbU648o6CvXxTx4SBMOw=
|
||||||
github.com/rivo/uniseg v0.4.3/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
github.com/rivo/uniseg v0.4.3/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||||
@ -315,7 +305,6 @@ github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcY
|
|||||||
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
|
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
|
||||||
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
|
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
|
||||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
github.com/yuin/goldmark v1.5.2 h1:ALmeCk/px5FSm1MAcFBAsVKZjDuMVj8Tm7FFIlMJnqU=
|
|
||||||
github.com/yuin/goldmark v1.5.2/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
github.com/yuin/goldmark v1.5.2/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
github.com/yuin/goldmark v1.5.3 h1:3HUJmBFbQW9fhQOzMgseU134xfi6hU+mjWywx5Ty+/M=
|
github.com/yuin/goldmark v1.5.3 h1:3HUJmBFbQW9fhQOzMgseU134xfi6hU+mjWywx5Ty+/M=
|
||||||
github.com/yuin/goldmark v1.5.3/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
github.com/yuin/goldmark v1.5.3/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
@ -371,8 +360,6 @@ golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLL
|
|||||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
golang.org/x/net v0.0.0-20221002022538-bcab6841153b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
|
golang.org/x/net v0.0.0-20221002022538-bcab6841153b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
|
||||||
golang.org/x/net v0.3.0 h1:VWL6FNY2bEEmsGVKabSlHu5Irp34xmMRoqb/9lF9lxk=
|
|
||||||
golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
|
|
||||||
golang.org/x/net v0.4.0 h1:Q5QPcMlvfxFTAPV0+07Xz/MpK9NTXu2VDUuy0FeMfaU=
|
golang.org/x/net v0.4.0 h1:Q5QPcMlvfxFTAPV0+07Xz/MpK9NTXu2VDUuy0FeMfaU=
|
||||||
golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
|
golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
|
||||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||||
|
@ -10,7 +10,6 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.rob.mx/nidito/puerta/internal/door"
|
|
||||||
"github.com/alexedwards/scs/v2"
|
"github.com/alexedwards/scs/v2"
|
||||||
"github.com/go-webauthn/webauthn/webauthn"
|
"github.com/go-webauthn/webauthn/webauthn"
|
||||||
"github.com/julienschmidt/httprouter"
|
"github.com/julienschmidt/httprouter"
|
||||||
@ -22,24 +21,19 @@ type AuthContext string
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
ContextCookieName AuthContext = "_puerta"
|
ContextCookieName AuthContext = "_puerta"
|
||||||
ContextSessionName AuthContext = "_rex"
|
ContextUser AuthContext = "_user"
|
||||||
ContextUserName AuthContext = "auth-username"
|
|
||||||
ContextGreeting AuthContext = "auth-greeting"
|
|
||||||
ContextDoor AuthContext = "auth-door"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Manager struct {
|
type Manager struct {
|
||||||
db db.Session
|
db db.Session
|
||||||
door door.Door
|
|
||||||
wan *webauthn.WebAuthn
|
wan *webauthn.WebAuthn
|
||||||
sess *scs.SessionManager
|
sess *scs.SessionManager
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewManager(wan *webauthn.WebAuthn, door door.Door, db db.Session) *Manager {
|
func NewManager(wan *webauthn.WebAuthn, db db.Session) *Manager {
|
||||||
sessionManager := scs.New()
|
sessionManager := scs.New()
|
||||||
sessionManager.Lifetime = 5 * time.Minute
|
sessionManager.Lifetime = 5 * time.Minute
|
||||||
return &Manager{
|
return &Manager{
|
||||||
door: door,
|
|
||||||
db: db,
|
db: db,
|
||||||
wan: wan,
|
wan: wan,
|
||||||
sess: sessionManager,
|
sess: sessionManager,
|
||||||
@ -101,76 +95,94 @@ func (am *Manager) NewSession(w http.ResponseWriter, req *http.Request, ps httpr
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (am *Manager) Protected(handler httprouter.Handle, redirect, enforce2FA bool) httprouter.Handle {
|
func (am *Manager) withUser(handler httprouter.Handle) httprouter.Handle {
|
||||||
|
|
||||||
return func(w http.ResponseWriter, req *http.Request, ps httprouter.Params) {
|
return func(w http.ResponseWriter, req *http.Request, ps httprouter.Params) {
|
||||||
ctx := req.Context()
|
ctxUser := req.Context().Value(ContextUser)
|
||||||
var user *User
|
req = func() *http.Request {
|
||||||
if ctxUser := ctx.Value(ContextUserName); ctxUser == nil {
|
if ctxUser != nil {
|
||||||
|
return req
|
||||||
|
}
|
||||||
|
|
||||||
cookie, err := req.Cookie(string(ContextCookieName))
|
cookie, err := req.Cookie(string(ContextCookieName))
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Debugf("no cookie value found in <%s>", req.Cookies())
|
logrus.Debugf("no cookie for user found in jar <%s>", req.Cookies())
|
||||||
if redirect {
|
return req
|
||||||
http.Redirect(w, req, "/login", http.StatusTemporaryRedirect)
|
|
||||||
} else {
|
|
||||||
am.requestAuth(w, http.StatusUnauthorized)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
session := &SessionUser{}
|
||||||
q := am.db.SQL().
|
q := am.db.SQL().
|
||||||
Select("s.token as token, ", "u.*").
|
Select("s.token as token, ", "u.*").
|
||||||
From("session as s").
|
From("session as s").
|
||||||
Join("user as u").On("s.user = u.id").
|
Join("user as u").On("s.user = u.id").
|
||||||
Where(db.Cond{"s.token": cookie.Value})
|
Where(db.Cond{"s.token": cookie.Value})
|
||||||
|
|
||||||
session := &SessionUser{}
|
|
||||||
if err := q.One(&session); err != nil {
|
if err := q.One(&session); err != nil {
|
||||||
|
logrus.Debugf("no cookie found in DB for jar <%s>: %s", req.Cookies(), err)
|
||||||
w.Header().Add("Set-Cookie", fmt.Sprintf("%s=%s; Max-Age=%d; Secure; Path=/;", ContextCookieName, "", -1))
|
w.Header().Add("Set-Cookie", fmt.Sprintf("%s=%s; Max-Age=%d; Secure; Path=/;", ContextCookieName, "", -1))
|
||||||
if redirect {
|
return req
|
||||||
http.Redirect(w, req, "/login", http.StatusSeeOther)
|
}
|
||||||
} else {
|
|
||||||
|
if session.Expired() {
|
||||||
|
logrus.Debugf("expired cookie found in DB for jar <%s>", req.Cookies())
|
||||||
|
w.Header().Add("Set-Cookie", fmt.Sprintf("%s=%s; Max-Age=%d; Secure; Path=/;", ContextCookieName, "", -1))
|
||||||
|
err := am.db.Collection("session").Find(db.Cond{"token": cookie.Value}).Delete()
|
||||||
|
if err != nil {
|
||||||
|
logrus.Errorf("could not purge expired session from DB: %s", err)
|
||||||
|
}
|
||||||
|
return req
|
||||||
|
}
|
||||||
|
|
||||||
|
return req.WithContext(context.WithValue(req.Context(), ContextUser, &session.User))
|
||||||
|
}()
|
||||||
|
|
||||||
|
handler(w, req, ps)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (am *Manager) RequireAuthOrRedirect(handler httprouter.Handle, target string) httprouter.Handle {
|
||||||
|
return am.withUser(func(w http.ResponseWriter, req *http.Request, ps httprouter.Params) {
|
||||||
|
if req.Context().Value(ContextUser) == nil {
|
||||||
|
http.Redirect(w, req, target, http.StatusTemporaryRedirect)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
handler(w, req, ps)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (am *Manager) Enforce2FA(handler httprouter.Handle) httprouter.Handle {
|
||||||
|
return am.withUser(func(w http.ResponseWriter, req *http.Request, ps httprouter.Params) {
|
||||||
|
if req.Context().Value(ContextUser) == nil {
|
||||||
am.requestAuth(w, http.StatusUnauthorized)
|
am.requestAuth(w, http.StatusUnauthorized)
|
||||||
}
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := session.User.IsAllowed(time.Now()); err != nil {
|
user := req.Context().Value(ContextUser).(*User)
|
||||||
logrus.Errorf("Denying access to %s: %s", session.User.Name, err)
|
if !user.Require2FA {
|
||||||
am.requestAuth(w, http.StatusForbidden)
|
handler(w, req, ps)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
req = req.WithContext(context.WithValue(ctx, ContextUserName, session.User.Name))
|
|
||||||
req = req.WithContext(context.WithValue(req.Context(), ContextGreeting, session.User.Greeting))
|
|
||||||
req = req.WithContext(context.WithValue(req.Context(), ContextDoor, am.door))
|
|
||||||
logrus.Debug("found allowed user")
|
|
||||||
user = &session.User
|
|
||||||
}
|
|
||||||
|
|
||||||
if enforce2FA && user.Require2FA {
|
|
||||||
logrus.Debug("Enforcing 2fa for request")
|
logrus.Debug("Enforcing 2fa for request")
|
||||||
var err error
|
var err error
|
||||||
err = user.FetchCredentials(am.db)
|
err = user.FetchCredentials(am.db)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Errorf("Failed fetching credentials: %s", err.Error())
|
logrus.Errorf("Failed fetching credentials: %s", err.Error())
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(user.credentials) == 0 {
|
if len(user.credentials) == 0 {
|
||||||
err = am.WebAuthnRegister(user, req)
|
err = am.WebAuthnRegister(req)
|
||||||
} else {
|
} else {
|
||||||
err = am.WebAuthnLogin(user, req)
|
err = am.WebAuthnLogin(req)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if wafc, ok := err.(WebAuthFlowChallenge); ok {
|
if wafc, ok := err.(WebAuthFlowChallenge); ok {
|
||||||
logrus.Debugf("Issuing challenge")
|
|
||||||
w.WriteHeader(200)
|
w.WriteHeader(200)
|
||||||
w.Header().Add("content-type", "application/json")
|
w.Header().Add("content-type", "application/json")
|
||||||
w.Write([]byte(wafc.Error()))
|
w.Write([]byte(wafc.Error()))
|
||||||
logrus.Debugf("Issued challenge")
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -178,12 +190,31 @@ func (am *Manager) Protected(handler httprouter.Handle, redirect, enforce2FA boo
|
|||||||
w.WriteHeader(http.StatusInternalServerError)
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
|
||||||
handler(w, req, ps)
|
handler(w, req, ps)
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (am *Manager) WebAuthnRegister(user *User, req *http.Request) error {
|
func (am *Manager) RequireAdmin(handler httprouter.Handle) httprouter.Handle {
|
||||||
|
return am.withUser(func(w http.ResponseWriter, req *http.Request, ps httprouter.Params) {
|
||||||
|
if req.Context().Value(ContextUser) == nil {
|
||||||
|
am.requestAuth(w, http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
user := req.Context().Value(ContextUser).(*User)
|
||||||
|
|
||||||
|
if !user.IsAdmin {
|
||||||
|
am.requestAuth(w, http.StatusUnauthorized)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
handler(w, req, ps)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (am *Manager) WebAuthnRegister(req *http.Request) error {
|
||||||
|
user := UserFromContext(req)
|
||||||
sd := am.sess.GetBytes(req.Context(), "wan-register")
|
sd := am.sess.GetBytes(req.Context(), "wan-register")
|
||||||
if sd == nil {
|
if sd == nil {
|
||||||
logrus.Infof("Starting webauthn registration for %s", user.Name)
|
logrus.Infof("Starting webauthn registration for %s", user.Name)
|
||||||
@ -228,7 +259,8 @@ func (am *Manager) WebAuthnRegister(user *User, req *http.Request) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (am *Manager) WebAuthnLogin(user *User, req *http.Request) error {
|
func (am *Manager) WebAuthnLogin(req *http.Request) error {
|
||||||
|
user := UserFromContext(req)
|
||||||
sd := am.sess.GetBytes(req.Context(), "rex")
|
sd := am.sess.GetBytes(req.Context(), "rex")
|
||||||
if sd == nil {
|
if sd == nil {
|
||||||
logrus.Infof("Starting webauthn login flow for %s", user.Name)
|
logrus.Infof("Starting webauthn login flow for %s", user.Name)
|
||||||
|
@ -8,16 +8,14 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Duration time.Duration
|
var DefaultTTL = TTL("30d")
|
||||||
|
|
||||||
func (d Duration) MarshalDB() (any, error) {
|
type TTL string
|
||||||
return time.Duration(d).String(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Duration) UnmarshalDB(value any) error {
|
func (ttl TTL) ToDuration() (res time.Duration, err error) {
|
||||||
str := value.(string)
|
suffix := ttl[len(ttl)-1]
|
||||||
suffix := str[len(str)-1]
|
|
||||||
|
|
||||||
|
toParse := string(ttl)
|
||||||
if suffix == 'd' || suffix == 'w' || suffix == 'M' {
|
if suffix == 'd' || suffix == 'w' || suffix == 'M' {
|
||||||
multiplier := 1
|
multiplier := 1
|
||||||
switch suffix {
|
switch suffix {
|
||||||
@ -28,29 +26,32 @@ func (d *Duration) UnmarshalDB(value any) error {
|
|||||||
case 'M':
|
case 'M':
|
||||||
multiplier = 24 * 7 * 30
|
multiplier = 24 * 7 * 30
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("unknown suffix for time duration %s", string(suffix))
|
err = fmt.Errorf("unknown suffix for time duration %s", string(suffix))
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
str = str[0 : len(str)-1]
|
toParse = toParse[0 : len(toParse)-1]
|
||||||
days, err := strconv.Atoi(str)
|
var days int
|
||||||
|
days, err = strconv.Atoi(toParse)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
str = fmt.Sprintf("%dh", days*multiplier)
|
toParse = fmt.Sprintf("%dh", days*multiplier)
|
||||||
}
|
}
|
||||||
tmp, err := time.ParseDuration(str)
|
res, err = time.ParseDuration(toParse)
|
||||||
if err != nil {
|
return
|
||||||
return err
|
|
||||||
}
|
|
||||||
*d = Duration(tmp)
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Duration) FromNow() time.Time {
|
func (ttl *TTL) FromNow() time.Time {
|
||||||
return time.Now().Add(time.Duration(*d))
|
d, _ := ttl.ToDuration()
|
||||||
|
return time.Now().Add(d)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Duration) Seconds() int {
|
func (ttl *TTL) Seconds() int {
|
||||||
return int(time.Duration(*d).Seconds())
|
d, _ := ttl.ToDuration()
|
||||||
|
return int(d.Seconds())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// var _ = (db.Unmarshaler(&TTL{}))
|
||||||
|
// var _ = (db.Marshaler(&TTL{}))
|
||||||
|
@ -10,6 +10,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
|
"github.com/upper/db/v4"
|
||||||
)
|
)
|
||||||
|
|
||||||
func parseHour(src string) (float64, error) {
|
func parseHour(src string) (float64, error) {
|
||||||
@ -40,13 +41,17 @@ type UserSchedule struct {
|
|||||||
hours []float64
|
hours []float64
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d UserSchedule) MarshalDB() ([]byte, error) {
|
func (d UserSchedule) MarshalDB() (any, error) {
|
||||||
return json.Marshal(d.src)
|
return json.Marshal(d.src)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *UserSchedule) UnmarshalDB(b []byte) error {
|
func (d UserSchedule) MarshalJSON() ([]byte, error) {
|
||||||
|
return json.Marshal(d.src)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *UserSchedule) UnmarshalDB(b any) error {
|
||||||
var v string
|
var v string
|
||||||
if err := json.Unmarshal(b, &v); err != nil {
|
if err := json.Unmarshal(b.([]byte), &v); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -104,3 +109,6 @@ func (sch *UserSchedule) AllowedAt(t time.Time) bool {
|
|||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var _ = (db.Unmarshaler(&UserSchedule{}))
|
||||||
|
var _ = (db.Marshaler(&UserSchedule{}))
|
||||||
|
@ -30,16 +30,26 @@ func (c *Credential) AsWebAuthn() webauthn.Credential {
|
|||||||
return *c.wan
|
return *c.wan
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func UserFromContext(req *http.Request) *User {
|
||||||
|
u := req.Context().Value(ContextUser)
|
||||||
|
|
||||||
|
if u != nil {
|
||||||
|
return u.(*User)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
type User struct {
|
type User struct {
|
||||||
ID int `db:"id"`
|
ID int `db:"id" json:"-"`
|
||||||
Handle string `db:"user"`
|
Handle string `db:"user" json:"user"`
|
||||||
Name string `db:"name"`
|
Name string `db:"name" json:"name"`
|
||||||
Password string `db:"password"`
|
Password string `db:"password" json:"password"`
|
||||||
Schedule *UserSchedule `db:"schedule,omitempty"`
|
Schedule *UserSchedule `db:"schedule,omitempty" json:"schedule"`
|
||||||
Expires *time.Time `db:"expires,omitempty"`
|
Expires *time.Time `db:"expires,omitempty" json:"expires"`
|
||||||
Greeting string `db:"greeting"`
|
Greeting string `db:"greeting" json:"greeting"`
|
||||||
TTL Duration `db:"max_ttl"`
|
TTL *TTL `db:"max_ttl,omitempty" json:"max_ttl"`
|
||||||
Require2FA bool `db:"second_factor"`
|
Require2FA bool `db:"second_factor" json:"second_factor"`
|
||||||
|
IsAdmin bool `db:"is_admin" json:"admin"`
|
||||||
credentials []*Credential
|
credentials []*Credential
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -92,7 +102,7 @@ func (u *User) FetchCredentials(sess db.Session) error {
|
|||||||
|
|
||||||
func (o *User) UnmarshalJSON(b []byte) error {
|
func (o *User) UnmarshalJSON(b []byte) error {
|
||||||
type alias User
|
type alias User
|
||||||
xo := &alias{TTL: Duration(30 * 24 * time.Hour)}
|
xo := &alias{TTL: &DefaultTTL}
|
||||||
if err := json.Unmarshal(b, xo); err != nil {
|
if err := json.Unmarshal(b, xo); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -129,3 +139,5 @@ func (user *User) Login(password string) error {
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var _ = db.Record(&User{})
|
||||||
|
@ -13,9 +13,10 @@ import (
|
|||||||
var (
|
var (
|
||||||
isOpening bool
|
isOpening bool
|
||||||
statusMu sync.Mutex
|
statusMu sync.Mutex
|
||||||
|
Active Door
|
||||||
)
|
)
|
||||||
|
|
||||||
type newDoorFunc func(map[string]any) Door
|
type newDoorFunc func(map[string]any) (Door, error)
|
||||||
|
|
||||||
var adapters = &struct {
|
var adapters = &struct {
|
||||||
factories map[string]newDoorFunc
|
factories map[string]newDoorFunc
|
||||||
@ -42,20 +43,20 @@ func setStatus(status bool) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// RequestToEnter opens the door unless it's already open or opening
|
// RequestToEnter opens the door unless it's already open or opening
|
||||||
func RequestToEnter(door Door, username string) error {
|
func RequestToEnter(username string) error {
|
||||||
statusMu.Lock()
|
statusMu.Lock()
|
||||||
if isOpening {
|
if isOpening {
|
||||||
defer statusMu.Unlock()
|
defer statusMu.Unlock()
|
||||||
return &DoorCommunicationError{"checking status", fmt.Errorf("Door is busy processing another request")}
|
return &ErrorCommunication{"checking status", fmt.Errorf("Door is busy processing another request")}
|
||||||
}
|
}
|
||||||
|
|
||||||
isOpen, err := door.IsOpen()
|
isOpen, err := Active.IsOpen()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
statusMu.Unlock()
|
statusMu.Unlock()
|
||||||
return &DoorCommunicationError{"checking status", err}
|
return &ErrorCommunication{"checking status", err}
|
||||||
} else if isOpen {
|
} else if isOpen {
|
||||||
statusMu.Unlock()
|
statusMu.Unlock()
|
||||||
return &DoorAlreadyOpen{}
|
return &ErrorAlreadyOpen{}
|
||||||
}
|
}
|
||||||
|
|
||||||
// okay, we're triggering an open and preventing others
|
// okay, we're triggering an open and preventing others
|
||||||
@ -66,11 +67,11 @@ func RequestToEnter(door Door, username string) error {
|
|||||||
|
|
||||||
errors := make(chan error, 2)
|
errors := make(chan error, 2)
|
||||||
done := make(chan bool)
|
done := make(chan bool)
|
||||||
go door.Open(errors, done)
|
go Active.Open(errors, done)
|
||||||
|
|
||||||
if err = <-errors; err != nil {
|
if err = <-errors; err != nil {
|
||||||
setStatus(false)
|
setStatus(false)
|
||||||
return &DoorCommunicationError{"opening", err}
|
return &ErrorCommunication{"opening", err}
|
||||||
}
|
}
|
||||||
|
|
||||||
logrus.Infof("Door opened for %s", username)
|
logrus.Infof("Door opened for %s", username)
|
||||||
@ -94,16 +95,17 @@ func RequestToEnter(door Door, username string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewDoor(config map[string]any) (Door, error) {
|
func Connect(config map[string]any) (err error) {
|
||||||
adapterName, hasAdapter := config["kind"]
|
adapterName, hasAdapter := config["kind"]
|
||||||
if !hasAdapter {
|
if !hasAdapter {
|
||||||
return nil, fmt.Errorf("missing DOOR_ADAPTER")
|
return fmt.Errorf("missing DOOR_ADAPTER")
|
||||||
}
|
}
|
||||||
|
|
||||||
factory, exists := adapters.factories[adapterName.(string)]
|
factory, exists := adapters.factories[adapterName.(string)]
|
||||||
if !exists {
|
if !exists {
|
||||||
return nil, fmt.Errorf("unknown DOOR_ADAPTER \"%s\", not one of [%s]", adapterName, strings.Join(adapters.names, ","))
|
return fmt.Errorf("unknown DOOR_ADAPTER \"%s\", not one of [%s]", adapterName, strings.Join(adapters.names, ","))
|
||||||
}
|
}
|
||||||
|
|
||||||
return factory(config), nil
|
Active, err = factory(config)
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
@ -7,25 +7,39 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
)
|
)
|
||||||
|
|
||||||
type DoorCommunicationError struct {
|
type ErrorCommunication struct {
|
||||||
during string
|
during string
|
||||||
err error
|
err error
|
||||||
}
|
}
|
||||||
|
|
||||||
func (err *DoorCommunicationError) Error() string {
|
func (err *ErrorCommunication) Error() string {
|
||||||
return fmt.Sprintf("ould not get door status while %s: %s", err.during, err.err.Error())
|
return fmt.Sprintf("could not get door status while %s: %s", err.during, err.err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (err *DoorCommunicationError) Code() int {
|
func (err *ErrorCommunication) Code() int {
|
||||||
return http.StatusInternalServerError
|
return http.StatusInternalServerError
|
||||||
}
|
}
|
||||||
|
|
||||||
type DoorAlreadyOpen struct{}
|
func (err *ErrorCommunication) Name() string {
|
||||||
|
return "communication-error"
|
||||||
|
}
|
||||||
|
|
||||||
func (err *DoorAlreadyOpen) Error() string {
|
type ErrorAlreadyOpen struct{}
|
||||||
|
|
||||||
|
func (err *ErrorAlreadyOpen) Error() string {
|
||||||
return "door is already open"
|
return "door is already open"
|
||||||
}
|
}
|
||||||
|
|
||||||
func (err *DoorAlreadyOpen) Code() int {
|
func (err *ErrorAlreadyOpen) Code() int {
|
||||||
return http.StatusPreconditionFailed
|
return http.StatusPreconditionFailed
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (err *ErrorAlreadyOpen) Name() string {
|
||||||
|
return "already-open"
|
||||||
|
}
|
||||||
|
|
||||||
|
type Error interface {
|
||||||
|
Error() string
|
||||||
|
Code() int
|
||||||
|
Name() string
|
||||||
|
}
|
||||||
|
@ -28,7 +28,7 @@ func init() {
|
|||||||
_register("hue", NewHue)
|
_register("hue", NewHue)
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewHue(config map[string]any) Door {
|
func NewHue(config map[string]any) (Door, error) {
|
||||||
|
|
||||||
cfg := &HueConfig{
|
cfg := &HueConfig{
|
||||||
ip: config["ip"].(string),
|
ip: config["ip"].(string),
|
||||||
@ -49,11 +49,11 @@ func NewHue(config map[string]any) Door {
|
|||||||
if cfg.username != "" && cfg.device > -1 {
|
if cfg.username != "" && cfg.device > -1 {
|
||||||
device, err := h.bridge.GetLight(cfg.device)
|
device, err := h.bridge.GetLight(cfg.device)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
return nil, err
|
||||||
}
|
}
|
||||||
h.device = device
|
h.device = device
|
||||||
}
|
}
|
||||||
return h
|
return h, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Hue) Setup(domain string) error {
|
func (h *Hue) Setup(domain string) error {
|
||||||
|
@ -18,11 +18,11 @@ type mockDoor struct {
|
|||||||
FailedToClose error
|
FailedToClose error
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewMock(config map[string]any) Door {
|
func NewMock(config map[string]any) (Door, error) {
|
||||||
logrus.Info("Initializing mock client")
|
logrus.Info("Initializing mock client")
|
||||||
return &mockDoor{
|
return &mockDoor{
|
||||||
Status: false,
|
Status: false,
|
||||||
}
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (md *mockDoor) IsOpen() (bool, error) {
|
func (md *mockDoor) IsOpen() (bool, error) {
|
||||||
|
@ -24,12 +24,12 @@ type Wemo struct {
|
|||||||
client *http.Client
|
client *http.Client
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewWemo(config map[string]any) Door {
|
func NewWemo(config map[string]any) (Door, error) {
|
||||||
logrus.Infof("Wemo client for %s starting", config["endpoint"])
|
logrus.Infof("Wemo client for %s starting", config["endpoint"])
|
||||||
return &Wemo{
|
return &Wemo{
|
||||||
endpoint: config["endpoint"].(string),
|
endpoint: config["endpoint"].(string),
|
||||||
client: &http.Client{Timeout: 4 * time.Second},
|
client: &http.Client{Timeout: 4 * time.Second},
|
||||||
}
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
const wemoBodyGet string = `<?xml version="1.0" encoding="utf-8"?>
|
const wemoBodyGet string = `<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
155
internal/server/admin.go
Normal file
155
internal/server/admin.go
Normal file
@ -0,0 +1,155 @@
|
|||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
// Copyright © 2022 Roberto Hidalgo <nidito@un.rob.mx>
|
||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.rob.mx/nidito/puerta/internal/auth"
|
||||||
|
"github.com/julienschmidt/httprouter"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
"github.com/upper/db/v4"
|
||||||
|
"golang.org/x/crypto/bcrypt"
|
||||||
|
)
|
||||||
|
|
||||||
|
func sendError(w http.ResponseWriter, err error) {
|
||||||
|
logrus.Error(err)
|
||||||
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeJSON(w http.ResponseWriter, data any) error {
|
||||||
|
res, err := json.Marshal(data)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Add("content-type", "application/json")
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
_, err = w.Write(res)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func listUsers(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
|
||||||
|
users := []*auth.User{}
|
||||||
|
if err := _db.Collection("user").Find().All(&users); err != nil {
|
||||||
|
sendError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
writeJSON(w, users)
|
||||||
|
}
|
||||||
|
|
||||||
|
func userFromRequest(r *http.Request, user *auth.User) (*auth.User, error) {
|
||||||
|
r.ParseForm()
|
||||||
|
if user == nil {
|
||||||
|
user = &auth.User{}
|
||||||
|
}
|
||||||
|
|
||||||
|
isAdmin, err := strconv.ParseBool(r.FormValue("is_admin"))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
secondFactor, err := strconv.ParseBool(r.FormValue("second_factor"))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
user.Handle = r.FormValue("handle")
|
||||||
|
user.Name = r.FormValue("name")
|
||||||
|
user.Greeting = r.FormValue("greeting")
|
||||||
|
user.Require2FA = secondFactor
|
||||||
|
user.IsAdmin = isAdmin
|
||||||
|
|
||||||
|
if r.FormValue("password") != "" {
|
||||||
|
password, err := bcrypt.GenerateFromPassword([]byte(r.FormValue("password")), bcrypt.DefaultCost)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
user.Password = string(password)
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.Form.Has("schedule") {
|
||||||
|
schedule := &auth.UserSchedule{}
|
||||||
|
err := schedule.UnmarshalDB([]byte(r.FormValue("schedule")))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
user.Schedule = schedule
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.Form.Has("expires") {
|
||||||
|
expires, err := time.Parse(time.RFC3339, r.FormValue("expires"))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
user.Expires = &expires
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.Form.Has("max_ttl") {
|
||||||
|
*user.TTL = auth.TTL(r.FormValue("max_ttl"))
|
||||||
|
}
|
||||||
|
|
||||||
|
return user, nil
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func createUser(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
|
||||||
|
user, err := userFromRequest(r, nil)
|
||||||
|
if err != nil {
|
||||||
|
sendError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := _db.Collection("user").Insert(user); err != nil {
|
||||||
|
sendError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusCreated)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getUser(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
|
||||||
|
var user *auth.User
|
||||||
|
idString := params.ByName("id")
|
||||||
|
|
||||||
|
if err := _db.Collection("user").Find(db.Cond{"handle": idString}).One(&user); err != nil {
|
||||||
|
sendError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
writeJSON(w, user)
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateUser(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
|
||||||
|
var user *auth.User
|
||||||
|
if err := _db.Collection("user").Find(db.Cond{"handle": params.ByName("id")}).One(user); err != nil {
|
||||||
|
http.NotFound(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
user, err := userFromRequest(r, user)
|
||||||
|
if err != nil {
|
||||||
|
sendError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := _db.Collection("user").UpdateReturning(user); err != nil {
|
||||||
|
sendError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusNoContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
func deleteUser(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
|
||||||
|
err := _db.Collection("user").Find(db.Cond{"handle": params.ByName("id")}).Delete()
|
||||||
|
if err != nil {
|
||||||
|
sendError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusNoContent)
|
||||||
|
}
|
102
internal/server/admin.html
Normal file
102
internal/server/admin.html
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>puerta@nidi.to</title>
|
||||||
|
<link rel="stylesheet" href="https://cdn.rob.mx/css/fonts.css" />
|
||||||
|
<link rel="stylesheet" href="https://cdn.rob.mx/nidito/index.css" />
|
||||||
|
<link rel="stylesheet" href="/static/index.css" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header id="main-header">
|
||||||
|
<div class="container">
|
||||||
|
<h1>Puerta</h1>
|
||||||
|
<p>Admin</p>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<main class="container">
|
||||||
|
<section id="create-user">
|
||||||
|
<h2>Crear usuario</h2>
|
||||||
|
<form id="create-user" method="post" action="/api/user">
|
||||||
|
<label for="user">Handle</label>
|
||||||
|
<input name="handle" placeholder="joao" autocorrect="off"/>
|
||||||
|
|
||||||
|
<label for="name">Nombre</label>
|
||||||
|
<input name="name" placeholder="João Gilberto" />
|
||||||
|
|
||||||
|
<label for="greeting">Greeting</label>
|
||||||
|
<input name="greeting" placeholder="Olá Joãzinho!" />
|
||||||
|
|
||||||
|
<label for="password">Password</label>
|
||||||
|
<input type="password" name="password" />
|
||||||
|
|
||||||
|
<label for="schedule">Horarios</label>
|
||||||
|
<input type="text" name="schedule" placeholder="days=1-5 hours=8-20:35" autocorrect="off"/>
|
||||||
|
|
||||||
|
<label for="expires">Expires</label>
|
||||||
|
<input type="datetime-local" name="expires" placeholder="2023-01-01T00:00:00Z" />
|
||||||
|
|
||||||
|
<label for="max_ttl">TTL</label>
|
||||||
|
<input type="text" name="max_ttl" placeholder="30d" autocorrect="off"/>
|
||||||
|
|
||||||
|
<label for="admin">Admin?</label>
|
||||||
|
<input type="checkbox" name="is_admin" />
|
||||||
|
|
||||||
|
<label for="second_factor">Requiere 2FA?</label>
|
||||||
|
<input type="checkbox" name="second_factor" />
|
||||||
|
|
||||||
|
<button id="create-user-submit" type="submit">Crear</button>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="user-list">
|
||||||
|
<h2>Usuarios</h2>
|
||||||
|
<ul></ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
|
||||||
|
</main>
|
||||||
|
<script type="module" src="/static/admin.js"></script>
|
||||||
|
|
||||||
|
<template id="user-info-panel">
|
||||||
|
<li class="user-info-panel {{ if .IsAdmin }}user-info-panel-admin{{ end }}">
|
||||||
|
<header>
|
||||||
|
<h3><slot name="name">{{ .Name }}</h3>
|
||||||
|
<code><pre>{{ .Handle }}</pre></code>
|
||||||
|
<button class="user-edit">Modificar</button>
|
||||||
|
</header>
|
||||||
|
<div class="user-info-panel-details">
|
||||||
|
<label for="name">Nombre</label>
|
||||||
|
<input name="name" value="" placeholder="João Gilberto" />
|
||||||
|
|
||||||
|
<label for="greeting">Greeting</label>
|
||||||
|
<input name="greeting" placeholder="Olá Joãzinho!" />
|
||||||
|
|
||||||
|
<label for="password">Password</label>
|
||||||
|
<input type="password" name="password" />
|
||||||
|
|
||||||
|
<label for="schedule">Horarios</label>
|
||||||
|
<input type="text" name="schedule" placeholder="days=1-5 hours=8-20:35" autocorrect="off"/>
|
||||||
|
|
||||||
|
<label for="expires">Expires</label>
|
||||||
|
<input type="datetime-local" name="expires" placeholder="2023-01-01T00:00:00Z" />
|
||||||
|
|
||||||
|
<label for="ttl">TTL</label>
|
||||||
|
<input type="text" name="max_ttl" placeholder="30d" autocorrect="off"/>
|
||||||
|
|
||||||
|
<label for="admin">Admin?</label>
|
||||||
|
<input type="checkbox" name="is_admin" />
|
||||||
|
|
||||||
|
<label for="admin">Requiere 2FA?</label>
|
||||||
|
<input type="checkbox" name="second_factor" />
|
||||||
|
|
||||||
|
<button class="user-delete">Eliminar</button>
|
||||||
|
<button class="user-save">Guardar cambios</button>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</template>
|
||||||
|
</body>
|
||||||
|
</html>
|
@ -8,12 +8,14 @@ import (
|
|||||||
"io/fs"
|
"io/fs"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
"git.rob.mx/nidito/puerta/internal/auth"
|
"git.rob.mx/nidito/puerta/internal/auth"
|
||||||
"git.rob.mx/nidito/puerta/internal/door"
|
"git.rob.mx/nidito/puerta/internal/door"
|
||||||
"git.rob.mx/nidito/puerta/internal/errors"
|
"git.rob.mx/nidito/puerta/internal/errors"
|
||||||
"github.com/go-webauthn/webauthn/webauthn"
|
"github.com/go-webauthn/webauthn/webauthn"
|
||||||
"github.com/julienschmidt/httprouter"
|
"github.com/julienschmidt/httprouter"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
"github.com/upper/db/v4"
|
"github.com/upper/db/v4"
|
||||||
"github.com/upper/db/v4/adapter/sqlite"
|
"github.com/upper/db/v4/adapter/sqlite"
|
||||||
)
|
)
|
||||||
@ -24,6 +26,9 @@ var loginTemplate []byte
|
|||||||
//go:embed index.html
|
//go:embed index.html
|
||||||
var indexTemplate []byte
|
var indexTemplate []byte
|
||||||
|
|
||||||
|
//go:embed admin.html
|
||||||
|
var adminTemplate []byte
|
||||||
|
|
||||||
//go:embed static/*
|
//go:embed static/*
|
||||||
var staticFiles embed.FS
|
var staticFiles embed.FS
|
||||||
|
|
||||||
@ -50,6 +55,41 @@ func ConfigDefaults(dbPath string) *Config {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type auditLog struct {
|
||||||
|
Timestamp time.Time `db:"timestamp"`
|
||||||
|
User string `db:"user"`
|
||||||
|
SecondFactor bool `db:"second_factor"`
|
||||||
|
Failure string `db:"failure"`
|
||||||
|
Err string `db:"error"`
|
||||||
|
Success bool `db:"success"`
|
||||||
|
IpAddress string `db:"ip_address"`
|
||||||
|
UserAgent string `db:"user_agent"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func newAuditLog(r *http.Request, err error) *auditLog {
|
||||||
|
user := auth.UserFromContext(r)
|
||||||
|
ip := r.RemoteAddr
|
||||||
|
ua := r.Header.Get("user-agent")
|
||||||
|
|
||||||
|
al := &auditLog{
|
||||||
|
Timestamp: time.Now(),
|
||||||
|
User: user.Handle,
|
||||||
|
SecondFactor: user.Require2FA,
|
||||||
|
IpAddress: ip,
|
||||||
|
UserAgent: ua,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
al.Failure = err.Error()
|
||||||
|
if derr, ok := err.(door.Error); ok {
|
||||||
|
al.Err = derr.Name()
|
||||||
|
al.Failure = derr.Error()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return al
|
||||||
|
}
|
||||||
|
|
||||||
func CORS(w http.ResponseWriter, r *http.Request) {
|
func CORS(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.Header.Get("Access-Control-Request-Method") != "" {
|
if r.Header.Get("Access-Control-Request-Method") != "" {
|
||||||
// Set CORS headers
|
// Set CORS headers
|
||||||
@ -63,9 +103,26 @@ func CORS(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func rex(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
|
func rex(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
|
||||||
userName := r.Context().Value(auth.ContextUserName).(string)
|
var err error
|
||||||
|
user := r.Context().Value(auth.ContextUser).(*auth.User)
|
||||||
|
|
||||||
if err := door.RequestToEnter(_door, userName); err != nil {
|
defer func() {
|
||||||
|
_, sqlErr := _db.Collection("log").Insert(newAuditLog(r, err))
|
||||||
|
if sqlErr != nil {
|
||||||
|
logrus.Errorf("could not record error log: %s", sqlErr)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
err = user.IsAllowed(time.Now())
|
||||||
|
if err != nil {
|
||||||
|
logrus.Errorf("Denying rex to %s: %s", user.Name, err)
|
||||||
|
http.Error(w, "Access denied", http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = door.RequestToEnter(user.Name)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
message, code := errors.ToHTTP(err)
|
message, code := errors.ToHTTP(err)
|
||||||
http.Error(w, message, code)
|
http.Error(w, message, code)
|
||||||
return
|
return
|
||||||
@ -75,7 +132,6 @@ func rex(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var _db db.Session
|
var _db db.Session
|
||||||
var _door door.Door
|
|
||||||
|
|
||||||
func Initialize(config *Config) (http.Handler, error) {
|
func Initialize(config *Config) (http.Handler, error) {
|
||||||
router := httprouter.New()
|
router := httprouter.New()
|
||||||
@ -94,8 +150,7 @@ func Initialize(config *Config) (http.Handler, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
_door, err = door.NewDoor(config.Adapter)
|
if err := door.Connect(config.Adapter); err != nil {
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -111,7 +166,7 @@ func Initialize(config *Config) (http.Handler, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
am := auth.NewManager(wan, _door, _db)
|
am := auth.NewManager(wan, _db)
|
||||||
|
|
||||||
serverRoot, err := fs.Sub(staticFiles, "static")
|
serverRoot, err := fs.Sub(staticFiles, "static")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -120,9 +175,16 @@ func Initialize(config *Config) (http.Handler, error) {
|
|||||||
|
|
||||||
router.ServeFiles("/static/*filepath", http.FS(serverRoot))
|
router.ServeFiles("/static/*filepath", http.FS(serverRoot))
|
||||||
router.GET("/login", renderTemplate(loginTemplate))
|
router.GET("/login", renderTemplate(loginTemplate))
|
||||||
router.GET("/", am.Protected(renderTemplate(indexTemplate), true, false))
|
router.GET("/", am.RequireAuthOrRedirect(renderTemplate(indexTemplate), "/login"))
|
||||||
router.POST("/api/login", am.NewSession)
|
router.POST("/api/login", am.NewSession)
|
||||||
router.POST("/api/rex", am.Protected(rex, false, true))
|
router.POST("/api/rex", am.Enforce2FA(rex))
|
||||||
|
router.GET("/admin", am.RequireAdmin(renderTemplate(adminTemplate)))
|
||||||
|
router.GET("/api/user", am.RequireAdmin(listUsers))
|
||||||
|
// router.GET("/api/user/:id", am.RequireAdmin(getUser))
|
||||||
|
router.GET("/api/user/:id", getUser)
|
||||||
|
router.PUT("/api/user", am.RequireAdmin(am.Enforce2FA(createUser)))
|
||||||
|
router.POST("/api/user/:id", am.RequireAdmin(am.Enforce2FA(updateUser)))
|
||||||
|
router.DELETE("/api/user/:id", am.RequireAdmin(am.Enforce2FA(deleteUser)))
|
||||||
|
|
||||||
return am.Route(router), nil
|
return am.Route(router), nil
|
||||||
}
|
}
|
||||||
|
70
internal/server/static/admin.js
Normal file
70
internal/server/static/admin.js
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
const button = document.querySelector("#open button")
|
||||||
|
const form = document.querySelector("#open")
|
||||||
|
const { create: createCredentials, get: getCredentials } = hankoWebAuthn;
|
||||||
|
|
||||||
|
const userList = document.querySelector("#user-list > ul")
|
||||||
|
|
||||||
|
|
||||||
|
customElements.define(
|
||||||
|
"user-info-panel",
|
||||||
|
class extends HTMLElement {
|
||||||
|
constructor() {
|
||||||
|
super()
|
||||||
|
let template = document.getElementById("user-info-panel")
|
||||||
|
const shadowRoot = this.attachShadow({ mode: "open" })
|
||||||
|
const panel = template.content.cloneNode(true)
|
||||||
|
|
||||||
|
panel.querySelector('h3').textContent = this.getAttribute('name')
|
||||||
|
panel.querySelector('name').value = this.getAttribute('name')
|
||||||
|
panel.querySelector('pre').textContent = this.getAttribute('handle')
|
||||||
|
|
||||||
|
panel.querySelector('greeting').value = this.getAttribute('greeting')
|
||||||
|
panel.querySelector('schedule').value = this.getAttribute('schedule')
|
||||||
|
panel.querySelector('expires').value = this.getAttribute('expires')
|
||||||
|
panel.querySelector('max_ttl').value = this.getAttribute('ttl')
|
||||||
|
panel.querySelector('is_admin').checked = this.hasAttribute('admin')
|
||||||
|
panel.querySelector('second_factor').checked = this.hasAttribute('second_factor')
|
||||||
|
shadowRoot.appendChild(panel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
async function fetchUsers() {
|
||||||
|
console.debug("fetching users")
|
||||||
|
let response = await window.fetch("/api/user")
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
alert("Could not load users")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let json = {}
|
||||||
|
try {
|
||||||
|
json = await response.json()
|
||||||
|
} catch (err) {
|
||||||
|
alert(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
userList.replaceChildren(json.map(u => {
|
||||||
|
const ip = document.createElement("user-info-panel")
|
||||||
|
ip.setAttribute("name", u.name)
|
||||||
|
ip.setAttribute("handle", u.handle)
|
||||||
|
|
||||||
|
ip.setAttribute('greeting', u.greeting)
|
||||||
|
ip.setAttribute('schedule', u.schedule)
|
||||||
|
ip.setAttribute('expires', u.expires)
|
||||||
|
ip.setAttribute('max_ttl', u.ttl)
|
||||||
|
if (u.admin) {
|
||||||
|
ip.setAttribute('is_admin', "")
|
||||||
|
}
|
||||||
|
if (u.second_factor) {
|
||||||
|
ip.setAttribute('second_factor', "")
|
||||||
|
}
|
||||||
|
|
||||||
|
return ip
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -4,10 +4,11 @@ importScripts(
|
|||||||
|
|
||||||
workbox.loadModule('workbox-strategies');
|
workbox.loadModule('workbox-strategies');
|
||||||
|
|
||||||
|
|
||||||
self.addEventListener("install", event => {
|
self.addEventListener("install", event => {
|
||||||
console.log("Service worker installed");
|
console.log("Service worker installed");
|
||||||
|
|
||||||
const urlsToCache = ["/", "app.js", "styles.css", "logo.svg"];
|
const urlsToCache = ["/login", "/", "index.css", "/index.js", "/login.js"];
|
||||||
event.waitUntil(
|
event.waitUntil(
|
||||||
caches.open("pwa-assets")
|
caches.open("pwa-assets")
|
||||||
.then(cache => {
|
.then(cache => {
|
||||||
@ -22,7 +23,7 @@ self.addEventListener("activate", event => {
|
|||||||
|
|
||||||
|
|
||||||
self.addEventListener('fetch', event => {
|
self.addEventListener('fetch', event => {
|
||||||
if (event.request.url.endsWith('.png')) {
|
if (event.request.url.endsWith('.js') || event.request.url.endsWith('.css')) {
|
||||||
const cacheFirst = new workbox.strategies.CacheFirst();
|
const cacheFirst = new workbox.strategies.CacheFirst();
|
||||||
event.respondWith(cacheFirst.handle({request: event.request}));
|
event.respondWith(cacheFirst.handle({request: event.request}));
|
||||||
}
|
}
|
||||||
|
30
schema.sql
30
schema.sql
@ -1,21 +1,23 @@
|
|||||||
|
|
||||||
CREATE TABLE user(
|
CREATE TABLE user(
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
name VARCHAR(255) NOT NULL UNIQUE,
|
handle VARCHAR(255) NOT NULL UNIQUE,
|
||||||
|
name TEXT NOT NULL,
|
||||||
password TEXT,
|
password TEXT,
|
||||||
expires TEXT, -- datetime
|
expires TEXT, -- datetime
|
||||||
greeting TEXT,
|
greeting TEXT,
|
||||||
max_ttl TEXT DEFAULT "30d", -- golang auth.Duration
|
max_ttl TEXT DEFAULT "30d", -- golang auth.TTL
|
||||||
|
schedule TEXT, -- golang auth.UserSchedule
|
||||||
second_factor BOOLEAN DEFAULT 1,
|
second_factor BOOLEAN DEFAULT 1,
|
||||||
schedule TEXT -- golang auth.Schedule
|
is_admin BOOLEAN DEFAULT 0 NOT NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE INDEX user_id ON user(id);
|
CREATE INDEX user_id ON user(id);
|
||||||
CREATE INDEX user_name ON user(name);
|
CREATE INDEX user_handle ON user(handle);
|
||||||
|
|
||||||
CREATE TABLE credential(
|
CREATE TABLE credential(
|
||||||
user INTEGER NOT NULL,
|
user INTEGER NOT NULL,
|
||||||
data text NOT NULL,
|
data TEXT NOT NULL,
|
||||||
FOREIGN KEY(user) REFERENCES user(id) ON DELETE CASCADE
|
FOREIGN KEY(user) REFERENCES user(id) ON DELETE CASCADE
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -31,11 +33,17 @@ CREATE TABLE session(
|
|||||||
|
|
||||||
CREATE INDEX session_token ON session(token);
|
CREATE INDEX session_token ON session(token);
|
||||||
|
|
||||||
|
CREATE TABLE log(
|
||||||
CREATE TABLE sessions (
|
timestamp TEXT PRIMARY KEY,
|
||||||
token TEXT PRIMARY KEY,
|
user TEXT NOT NULL,
|
||||||
data BLOB NOT NULL,
|
second_factor BOOLEAN NOT NULL,
|
||||||
expiry REAL NOT NULL
|
failure VARCHAR(255),
|
||||||
|
error TEXT,
|
||||||
|
success boolean NOT NULL,
|
||||||
|
ip_address varchar(255) NOT NULL,
|
||||||
|
user_agent varchar(255) NOT NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE INDEX sessions_expiry_idx ON sessions(expiry);
|
CREATE INDEX log_timestamp_idx ON log(timestamp);
|
||||||
|
CREATE INDEX log_timestamp_error_idx ON log(timestamp,error);
|
||||||
|
CREATE INDEX log_timestamp_user_idx ON log(timestamp,user);
|
||||||
|
Loading…
Reference in New Issue
Block a user