knock knock
This commit is contained in:
commit
e20c05bc74
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
puerta
|
201
LICENSE.txt
Normal file
201
LICENSE.txt
Normal file
@ -0,0 +1,201 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
145
cmd/admin/user.go
Normal file
145
cmd/admin/user.go
Normal file
@ -0,0 +1,145 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright © 2022 Roberto Hidalgo <nidito@un.rob.mx>
|
||||
package admin
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"git.rob.mx/nidito/chinampa"
|
||||
"git.rob.mx/nidito/chinampa/pkg/command"
|
||||
"git.rob.mx/nidito/puerta/internal/auth"
|
||||
"git.rob.mx/nidito/puerta/internal/server"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/upper/db/v4/adapter/sqlite"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
func init() {
|
||||
chinampa.Register(userAddCommand)
|
||||
}
|
||||
|
||||
var userAddCommand = &command.Command{
|
||||
Path: []string{"admin", "user", "create"},
|
||||
Summary: "Create the initial user",
|
||||
Description: "",
|
||||
Arguments: command.Arguments{
|
||||
{
|
||||
Name: "handle",
|
||||
Description: "the username to add",
|
||||
Required: true,
|
||||
},
|
||||
{
|
||||
Name: "name",
|
||||
Description: "the user's name",
|
||||
Required: true,
|
||||
},
|
||||
{
|
||||
Name: "password",
|
||||
Description: "the password to set for this user",
|
||||
Required: true,
|
||||
},
|
||||
},
|
||||
Options: command.Options{
|
||||
"config": {
|
||||
Type: "string",
|
||||
Default: "./config.joao.yaml",
|
||||
},
|
||||
"db": {
|
||||
Type: "string",
|
||||
Default: "./puerta.db",
|
||||
},
|
||||
"ttl": {
|
||||
Type: "string",
|
||||
Description: "the ttl to set for the user",
|
||||
Default: "30d",
|
||||
},
|
||||
"expires": {
|
||||
Type: "string",
|
||||
Description: "the max cookie lifetime",
|
||||
Default: "",
|
||||
},
|
||||
"schedule": {
|
||||
Type: "string",
|
||||
Description: "the schedule to set for the user",
|
||||
Default: "",
|
||||
},
|
||||
"greeting": {
|
||||
Type: "string",
|
||||
Description: "a custom greeting for the user",
|
||||
Default: "",
|
||||
},
|
||||
},
|
||||
Action: func(cmd *command.Command) error {
|
||||
config := cmd.Options["config"].ToValue().(string)
|
||||
db := cmd.Options["db"].ToValue().(string)
|
||||
|
||||
expires := cmd.Options["expires"].ToString()
|
||||
schedule := cmd.Options["schedule"].ToString()
|
||||
ttl := cmd.Options["ttl"].ToString()
|
||||
greeting := cmd.Options["greeting"].ToString()
|
||||
|
||||
data, err := os.ReadFile(config)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not read config file: %w", err)
|
||||
}
|
||||
|
||||
cfg := server.ConfigDefaults(db)
|
||||
|
||||
if err := yaml.Unmarshal(data, &cfg); err != nil {
|
||||
return fmt.Errorf("could not unserialize yaml at %s: %w", config, err)
|
||||
}
|
||||
|
||||
sess, err := sqlite.Open(sqlite.ConnectionURL{
|
||||
Database: cfg.DB,
|
||||
// Options: {},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
password, err := bcrypt.GenerateFromPassword([]byte(cmd.Arguments[2].ToString()), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var ttlDuration auth.Duration
|
||||
if err := ttlDuration.UnmarshalDB(ttl); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
user := &auth.User{
|
||||
Name: cmd.Arguments[0].ToString(),
|
||||
Password: string(password),
|
||||
Handle: cmd.Arguments[1].ToString(),
|
||||
Greeting: greeting,
|
||||
TTL: ttlDuration,
|
||||
}
|
||||
|
||||
if schedule != "" {
|
||||
user.Schedule = &auth.UserSchedule{}
|
||||
if err := user.Schedule.UnmarshalDB([]byte(schedule)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if expires != "" {
|
||||
t, err := time.Parse(time.RFC3339, expires)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
user.Expires = &t
|
||||
}
|
||||
|
||||
res, err := sess.Collection("user").Insert(user)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
logrus.Infof("Created user %s with ID: %d", user.Name, res.ID())
|
||||
return nil
|
||||
|
||||
},
|
||||
}
|
85
cmd/hue/main.go
Normal file
85
cmd/hue/main.go
Normal file
@ -0,0 +1,85 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright © 2022 Roberto Hidalgo <nidito@un.rob.mx>
|
||||
package hue
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"git.rob.mx/nidito/chinampa"
|
||||
"git.rob.mx/nidito/chinampa/pkg/command"
|
||||
"git.rob.mx/nidito/puerta/internal/door"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func init() {
|
||||
chinampa.Register(setupHueCommand)
|
||||
chinampa.Register(testHueCommand)
|
||||
}
|
||||
|
||||
var setupHueCommand = &command.Command{
|
||||
Path: []string{"hue", "setup"},
|
||||
Summary: "Creates a local hue user and finds out available plugs",
|
||||
Description: "",
|
||||
Arguments: command.Arguments{
|
||||
{
|
||||
Name: "ip",
|
||||
Description: "The ip address of the bridge",
|
||||
Required: true,
|
||||
},
|
||||
{
|
||||
Name: "domain",
|
||||
Description: "the domain or application name to use when registering",
|
||||
Default: "puerta.nidi.to",
|
||||
},
|
||||
},
|
||||
Action: func(cmd *command.Command) error {
|
||||
ip := cmd.Arguments[0].ToValue().(string)
|
||||
domain := cmd.Arguments[1].ToValue().(string)
|
||||
|
||||
logrus.Infof("Setting up with bridge at %s, app %s", ip, domain)
|
||||
d := door.NewHue(map[string]any{
|
||||
"ip": ip,
|
||||
"username": "",
|
||||
"device": -1,
|
||||
}).(*door.Hue)
|
||||
|
||||
return d.Setup(os.Args[2])
|
||||
},
|
||||
}
|
||||
|
||||
var testHueCommand = &command.Command{
|
||||
Path: []string{"hue", "test"},
|
||||
Summary: "Uses a given configuration to open door",
|
||||
Description: "",
|
||||
Arguments: command.Arguments{
|
||||
{
|
||||
Name: "ip",
|
||||
Description: "The ip address of the bridge",
|
||||
Required: true,
|
||||
},
|
||||
{
|
||||
Name: "username",
|
||||
Description: "An existing bridge username",
|
||||
Required: true,
|
||||
},
|
||||
{
|
||||
Name: "device",
|
||||
Description: "The device ID to test",
|
||||
Required: true,
|
||||
},
|
||||
},
|
||||
Action: func(cmd *command.Command) error {
|
||||
ip := cmd.Arguments[0].ToValue().(string)
|
||||
username := cmd.Arguments[1].ToValue().(string)
|
||||
device := cmd.Arguments[2].ToValue().(string)
|
||||
|
||||
logrus.Infof("Testing bridge at %s, username %s, device %s", ip, username, device)
|
||||
d := door.NewHue(map[string]any{
|
||||
"ip": ip,
|
||||
"username": username,
|
||||
"device": device,
|
||||
})
|
||||
|
||||
return door.RequestToEnter(d, "test")
|
||||
},
|
||||
}
|
61
cmd/server/main.go
Normal file
61
cmd/server/main.go
Normal file
@ -0,0 +1,61 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright © 2022 Roberto Hidalgo <nidito@un.rob.mx>
|
||||
package server
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"git.rob.mx/nidito/chinampa"
|
||||
"git.rob.mx/nidito/chinampa/pkg/command"
|
||||
"git.rob.mx/nidito/puerta/internal/server"
|
||||
"github.com/sirupsen/logrus"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
func init() {
|
||||
chinampa.Register(serverCommand)
|
||||
}
|
||||
|
||||
var serverCommand = &command.Command{
|
||||
Path: []string{"server"},
|
||||
Summary: "Runs the http server",
|
||||
Description: "",
|
||||
Options: command.Options{
|
||||
"config": {
|
||||
Type: "string",
|
||||
Default: "./config.joao.yaml",
|
||||
},
|
||||
"db": {
|
||||
Type: "string",
|
||||
Default: "./puerta.db",
|
||||
},
|
||||
},
|
||||
Action: func(cmd *command.Command) error {
|
||||
config := cmd.Options["config"].ToValue().(string)
|
||||
db := cmd.Options["db"].ToValue().(string)
|
||||
|
||||
data, err := os.ReadFile(config)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not read config file: %w", err)
|
||||
}
|
||||
|
||||
cfg := server.ConfigDefaults(db)
|
||||
|
||||
if err := yaml.Unmarshal(data, &cfg); err != nil {
|
||||
return fmt.Errorf("could not unserialize yaml at %s: %w", config, err)
|
||||
}
|
||||
|
||||
logger := logrus.New()
|
||||
logger.SetFormatter(&logrus.JSONFormatter{DisableTimestamp: false})
|
||||
|
||||
router, err := server.Initialize(cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
logrus.Infof("Listening on port %d", cfg.HTTP.Listen)
|
||||
return http.ListenAndServe(fmt.Sprintf(":%d", cfg.HTTP.Listen), router)
|
||||
},
|
||||
}
|
13
config.template.yaml
Normal file
13
config.template.yaml
Normal file
@ -0,0 +1,13 @@
|
||||
name: Casa de Alguien
|
||||
|
||||
adapter:
|
||||
kind: dry-run
|
||||
ip: 192.168.1.256
|
||||
username: nobody
|
||||
device: -1
|
||||
|
||||
http:
|
||||
listen: 8000
|
||||
domain: localhost
|
||||
|
||||
db: ./puerta.db
|
56
go.mod
Normal file
56
go.mod
Normal file
@ -0,0 +1,56 @@
|
||||
module git.rob.mx/nidito/puerta
|
||||
|
||||
go 1.18
|
||||
|
||||
require (
|
||||
git.rob.mx/nidito/chinampa v0.0.0-20221231055324-8ea5f42ef848
|
||||
github.com/alexedwards/scs/v2 v2.5.0
|
||||
github.com/amimof/huego v1.2.1
|
||||
github.com/go-webauthn/webauthn v0.6.0
|
||||
github.com/julienschmidt/httprouter v1.3.0
|
||||
github.com/sirupsen/logrus v1.9.0
|
||||
github.com/upper/db/v4 v4.6.0
|
||||
golang.org/x/crypto v0.4.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/alecthomas/chroma v0.10.0 // indirect
|
||||
github.com/aymanbagabas/go-osc52 v1.0.3 // indirect
|
||||
github.com/aymerick/douceur v0.2.0 // indirect
|
||||
github.com/charmbracelet/glamour v0.6.0 // indirect
|
||||
github.com/dlclark/regexp2 v1.4.0 // indirect
|
||||
github.com/fatih/color v1.13.0 // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.4.0 // indirect
|
||||
github.com/go-playground/locales v0.14.0 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.0 // indirect
|
||||
github.com/go-playground/validator/v10 v10.11.1 // indirect
|
||||
github.com/go-webauthn/revoke v0.1.6 // indirect
|
||||
github.com/golang-jwt/jwt/v4 v4.4.3 // indirect
|
||||
github.com/google/go-tpm v0.3.3 // indirect
|
||||
github.com/google/uuid v1.3.0 // indirect
|
||||
github.com/gorilla/css v1.0.0 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.0.1 // indirect
|
||||
github.com/leodido/go-urn v1.2.1 // indirect
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.17 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.14 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.16 // indirect
|
||||
github.com/microcosm-cc/bluemonday v1.0.21 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/muesli/reflow v0.3.0 // indirect
|
||||
github.com/muesli/termenv v0.13.0 // indirect
|
||||
github.com/olekukonko/tablewriter v0.0.5 // indirect
|
||||
github.com/rivo/uniseg v0.2.0 // indirect
|
||||
github.com/segmentio/fasthash v1.0.3 // indirect
|
||||
github.com/spf13/cobra v1.6.1 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
github.com/yuin/goldmark v1.5.2 // indirect
|
||||
github.com/yuin/goldmark-emoji v1.0.1 // indirect
|
||||
golang.org/x/net v0.3.0 // indirect
|
||||
golang.org/x/sys v0.3.0 // indirect
|
||||
golang.org/x/term v0.3.0 // indirect
|
||||
golang.org/x/text v0.5.0 // indirect
|
||||
)
|
482
go.sum
Normal file
482
go.sum
Normal file
@ -0,0 +1,482 @@
|
||||
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-20221229190558-4eec8e55a1e6/go.mod h1:nQlQqIQ6UuP6spFFZvfVT1MhQJYEA7B3Y2EtM2Fha3Y=
|
||||
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=
|
||||
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/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||
github.com/alecthomas/chroma v0.10.0 h1:7XDcGkCQopCNKjZHfYrNLraA+M7e0fMiJ/Mfikbfjek=
|
||||
github.com/alecthomas/chroma v0.10.0/go.mod h1:jtJATyUxlIORhUOFNA9NZDWGAQ8wpxQQqNSB4rjA/1s=
|
||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/alexedwards/scs/v2 v2.5.0 h1:zgxOfNFmiJyXG7UPIuw1g2b9LWBeRLh3PjfB9BDmfL4=
|
||||
github.com/alexedwards/scs/v2 v2.5.0/go.mod h1:ToaROZxyKukJKT/xLcVQAChi5k6+Pn1Gvmdl7h3RRj8=
|
||||
github.com/amimof/huego v1.2.1 h1:kd36vsieclW4fZ4Vqii9DNU2+6ptWWtkp4OG0AXM8HE=
|
||||
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/aymanbagabas/go-osc52 v1.0.3 h1:DTwqENW7X9arYimJrPeGZcV0ln14sGMt3pHZspWD+Mg=
|
||||
github.com/aymanbagabas/go-osc52 v1.0.3/go.mod h1:zT8H+Rk4VSabYN90pWyugflM3ZhpTZNC7cASDfUCdT4=
|
||||
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
|
||||
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
|
||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
|
||||
github.com/charmbracelet/glamour v0.6.0 h1:wi8fse3Y7nfcabbbDuwolqTqMQPMnVPeZhDM273bISc=
|
||||
github.com/charmbracelet/glamour v0.6.0/go.mod h1:taqWV4swIMMbWALc0m7AfE9JkPSU8om2538k9ITBxOc=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
|
||||
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
|
||||
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
|
||||
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
|
||||
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||
github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
|
||||
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
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/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/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/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M=
|
||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
|
||||
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/fxamacker/cbor/v2 v2.4.0 h1:ri0ArlOR+5XunOP8CRUowT0pSJOwhW098ZCUyskZD88=
|
||||
github.com/fxamacker/cbor/v2 v2.4.0/go.mod h1:TA1xS00nchWmaBnEIxPSE5oHLuJBAVvqrtAnWBwBCVo=
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
|
||||
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
||||
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
||||
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
|
||||
github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
|
||||
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU=
|
||||
github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs=
|
||||
github.com/go-playground/universal-translator v0.18.0 h1:82dyy6p4OuJq4/CByFNOn/jYrnRPArHwAcmLoJZxyho=
|
||||
github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA=
|
||||
github.com/go-playground/validator/v10 v10.11.1 h1:prmOlTVv+YjZjmRmNSF3VmspqJIxJWXmqUsHwfTRRkQ=
|
||||
github.com/go-playground/validator/v10 v10.11.1/go.mod h1:i+3WkQ1FvaUjjxh1kSvIA4dMGDBiPU55YFDl0WbKdWU=
|
||||
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/go-webauthn/revoke v0.1.6 h1:3tv+itza9WpX5tryRQx4GwxCCBrCIiJ8GIkOhxiAmmU=
|
||||
github.com/go-webauthn/revoke v0.1.6/go.mod h1:TB4wuW4tPlwgF3znujA96F70/YSQXHPPWl7vgY09Iy8=
|
||||
github.com/go-webauthn/webauthn v0.6.0 h1:uLInMApSvBfP+vEFasNE0rnVPG++fjp7lmAIvNhe+UU=
|
||||
github.com/go-webauthn/webauthn v0.6.0/go.mod h1:7edMRZXwuM6JIVjN68G24Bzt+bPCvTmjiL0j+cAmXtY=
|
||||
github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
|
||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
|
||||
github.com/golang-jwt/jwt/v4 v4.4.3 h1:Hxl6lhQFj4AnOX6MLrsCb/+7tCj7DxP7VA+2rDIq5AU=
|
||||
github.com/golang-jwt/jwt/v4 v4.4.3/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
|
||||
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
|
||||
github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-tpm v0.1.2-0.20190725015402-ae6dd98980d4/go.mod h1:H9HbmUG2YgV/PHITkO7p6wxEEj/v5nlsVWIwumwH2NI=
|
||||
github.com/google/go-tpm v0.3.0/go.mod h1:iVLWvrPp/bHeEkxTFi9WG6K9w0iy2yIszHwZGHPbzAw=
|
||||
github.com/google/go-tpm v0.3.3 h1:P/ZFNBZYXRxc+z7i5uyd8VP7MaDteuLZInzrH2idRGo=
|
||||
github.com/google/go-tpm v0.3.3/go.mod h1:9Hyn3rgnzWF9XBWVk6ml6A6hNkbWjNFlDQL51BeghL4=
|
||||
github.com/google/go-tpm-tools v0.0.0-20190906225433-1614c142f845/go.mod h1:AVfHadzbdzHo54inR2x1v640jdi1YSi3NauM2DUsxk0=
|
||||
github.com/google/go-tpm-tools v0.2.0/go.mod h1:npUd03rQ60lxN7tzeBJreG38RvWwme2N1reF/eeiBk4=
|
||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY=
|
||||
github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c=
|
||||
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
|
||||
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
|
||||
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
|
||||
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/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/ipfs/go-detect-race v0.0.1 h1:qX/xay2W3E4Q1U7d9lNs1sU9nvguX0a7319XbyQ6cOk=
|
||||
github.com/ipfs/go-detect-race v0.0.1/go.mod h1:8BNT7shDZPo99Q74BpGMK+4D8Mn4j46UU0LZ723meps=
|
||||
github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo=
|
||||
github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
|
||||
github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
|
||||
github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA=
|
||||
github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE=
|
||||
github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s=
|
||||
github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o=
|
||||
github.com/jackc/pgconn v1.9.0/go.mod h1:YctiPyvzfU11JFxoXokUOOKQXQmDMoJL9vJzHH8/2JY=
|
||||
github.com/jackc/pgconn v1.9.1-0.20210724152538-d89c8390a530/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI=
|
||||
github.com/jackc/pgconn v1.11.0/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI=
|
||||
github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8=
|
||||
github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE=
|
||||
github.com/jackc/pgmock v0.0.0-20201204152224-4fe30f7445fd/go.mod h1:hrBW0Enj2AZTNpt/7Y5rr2xe/9Mn757Wtb2xeBzPv2c=
|
||||
github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65/go.mod h1:5R2h2EEX+qri8jOWMbJCtaPWkrrNc7OHwsp2TCqp7ak=
|
||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||
github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78=
|
||||
github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA=
|
||||
github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg=
|
||||
github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM=
|
||||
github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM=
|
||||
github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
|
||||
github.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
|
||||
github.com/jackc/pgproto3/v2 v2.2.0/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
|
||||
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E=
|
||||
github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg=
|
||||
github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc=
|
||||
github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw=
|
||||
github.com/jackc/pgtype v1.8.1-0.20210724151600-32e20a603178/go.mod h1:C516IlIV9NKqfsMCXTdChteoXmwgUceqaLfjg2e3NlM=
|
||||
github.com/jackc/pgtype v1.10.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4=
|
||||
github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y=
|
||||
github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM=
|
||||
github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc=
|
||||
github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs=
|
||||
github.com/jackc/pgx/v4 v4.15.0/go.mod h1:D/zyOyXiaM1TmVWnOM18p0xdDtdakRBa0RsVGI3U3bw=
|
||||
github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
|
||||
github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
|
||||
github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
|
||||
github.com/jackc/puddle v1.2.1/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
|
||||
github.com/jarcoal/httpmock v1.0.4 h1:jp+dy/+nonJE4g4xbVtl9QdrUNbn6/3hDT5R4nDIZnA=
|
||||
github.com/jarcoal/httpmock v1.0.4/go.mod h1:ATjnClrvW/3tijVmpL/va5Z3aAyGvqU3gCT8nX0Txik=
|
||||
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
|
||||
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
||||
github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U=
|
||||
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
|
||||
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
|
||||
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w=
|
||||
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
|
||||
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/lib/pq v1.10.4/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||
github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
|
||||
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||
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.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
|
||||
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
|
||||
github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU=
|
||||
github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/mattn/go-sqlite3 v1.14.9/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
|
||||
github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
|
||||
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
github.com/microcosm-cc/bluemonday v1.0.21 h1:dNH3e4PSyE4vNX+KlRGHT5KrSvjeUkoNPwEORjffHJg=
|
||||
github.com/microcosm-cc/bluemonday v1.0.21/go.mod h1:ytNkv4RrDrLJ2pqlsSI46O6IVXmZOBBD4SaJyDwwTkM=
|
||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE=
|
||||
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
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/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
|
||||
github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
|
||||
github.com/muesli/termenv v0.13.0 h1:wK20DRpJdDX8b7Ek2QfhvqhRQFZ237RGRO0RQ/Iqdy0=
|
||||
github.com/muesli/termenv v0.13.0/go.mod h1:sP1+uffeLaEYpyOTb8pLCUctGcGLnoFjSn4YJK5e2bc=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
|
||||
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
|
||||
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
|
||||
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.8.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/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||
github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
|
||||
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
|
||||
github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
||||
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
||||
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
|
||||
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/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/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
||||
github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
|
||||
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
|
||||
github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
|
||||
github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU=
|
||||
github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc=
|
||||
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
|
||||
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
|
||||
github.com/segmentio/fasthash v1.0.3 h1:EI9+KE1EwvMLBWwjpRDc+fEM+prwxDYbslddQGtrmhM=
|
||||
github.com/segmentio/fasthash v1.0.3/go.mod h1:waKX8l2N8yckOgmSsXJi7x1ZfdKZ4x7KRMzBtS3oedY=
|
||||
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
|
||||
github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
|
||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
||||
github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0=
|
||||
github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
|
||||
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
||||
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
|
||||
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
||||
github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
|
||||
github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE=
|
||||
github.com/spf13/cobra v1.6.1 h1:o94oiPyS4KD1mPy2fmcYYHHfCxLqYjJOhGsCHFZtEzA=
|
||||
github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY=
|
||||
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
|
||||
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
|
||||
github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
|
||||
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
|
||||
github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
|
||||
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
|
||||
github.com/upper/db/v4 v4.6.0 h1:0VmASnqrl/XN8Ehoq++HBgZ4zRD5j3GXygW8FhP0C5I=
|
||||
github.com/upper/db/v4 v4.6.0/go.mod h1:2mnRcPf+RcCXmVcD+o04LYlyu3UuF7ubamJia7CkN6s=
|
||||
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
||||
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
||||
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/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-emoji v1.0.1 h1:ctuWEyzGBwiucEqxzwe0SOYDXPAucOrE9NQC18Wa1os=
|
||||
github.com/yuin/goldmark-emoji v1.0.1/go.mod h1:2w1E6FEWLcDQkoTE+7HU6QF1F6SLlNGjRIBbIZQFqkQ=
|
||||
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
|
||||
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
|
||||
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||
go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
|
||||
go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
|
||||
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
|
||||
go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4=
|
||||
go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU=
|
||||
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=
|
||||
go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
||||
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
||||
go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM=
|
||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
|
||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
|
||||
golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.0.0-20220307211146-efcb8507fb70/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.4.0 h1:UVQgzMY87xqpKNgb+kDsll2Igd33HszWHFLmpaRMq/8=
|
||||
golang.org/x/crypto v0.4.0/go.mod h1:3quD/ATkf6oY+rnes5c3ExXTbLc8mueNue5/DoinL80=
|
||||
golang.org/x/exp v0.0.0-20181106170214-d68db9428509/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
|
||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
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-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/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210629170331-7dc0b73dc9fb/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211210111614-af8b64212486/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ=
|
||||
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.3.0 h1:qoo4akIqOcDME5bhc/NgxUdovd6BSS2uMsVjB56q1xI=
|
||||
golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.5.0 h1:OLmvp0KP+FVG99Ct/qFiL/Fhk4zp4QQnZ7b2U+5piUM=
|
||||
golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
||||
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/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/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s=
|
||||
gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22 h1:VpOs+IwYnYBaFnrNAeB8UUWtL3vEUnzSCL1nVjPhqrw=
|
||||
gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA=
|
||||
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
|
||||
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
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-20210107192922-496545a6307b/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=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
||||
modernc.org/b v1.0.2/go.mod h1:fVGfCIzkZw5RsuF2A2WHbJmY7FiMIq30nP4s52uWsoY=
|
||||
modernc.org/db v1.0.3/go.mod h1:L4ltUg8tu2pkSJk+fKaRrXs/3EdW79ZKYQ5PfVDT53U=
|
||||
modernc.org/file v1.0.3/go.mod h1:CNj/pwOfCtCbqiHcXDUlHBB2vWrzdaDCWdcnjtS1+XY=
|
||||
modernc.org/fileutil v1.0.0/go.mod h1:JHsWpkrk/CnVV1H/eGlFf85BEpfkrp56ro8nojIq9Q8=
|
||||
modernc.org/golex v1.0.1/go.mod h1:QCA53QtsT1NdGkaZZkF5ezFwk4IXh4BGNafAARTC254=
|
||||
modernc.org/internal v1.0.0/go.mod h1:VUD/+JAkhCpvkUitlEOnhpVxCgsBI90oTzSCRcqQVSM=
|
||||
modernc.org/internal v1.0.2/go.mod h1:bycJAcev709ZU/47nil584PeBD+kbu8nv61ozeMso9E=
|
||||
modernc.org/lex v1.0.0/go.mod h1:G6rxMTy3cH2iA0iXL/HRRv4Znu8MK4higxph/lE7ypk=
|
||||
modernc.org/lexer v1.0.0/go.mod h1:F/Dld0YKYdZCLQ7bD0USbWL4YKCyTDRDHiDTOs0q0vk=
|
||||
modernc.org/lldb v1.0.2/go.mod h1:ovbKqyzA9H/iPwHkAOH0qJbIQVT9rlijecenxDwVUi0=
|
||||
modernc.org/mathutil v1.0.0/go.mod h1:wU0vUrJsVWBZ4P6e7xtFJEhFSNsfRLJ8H458uRjg03k=
|
||||
modernc.org/mathutil v1.1.1/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
|
||||
modernc.org/mathutil v1.2.2/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
|
||||
modernc.org/mathutil v1.4.1/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
|
||||
modernc.org/ql v1.4.0/go.mod h1:q4c29Bgdx+iAtxx47ODW5Xo2X0PDkjSCK9NdQl6KFxc=
|
||||
modernc.org/sortutil v1.1.0/go.mod h1:ZyL98OQHJgH9IEfN71VsamvJgrtRX9Dj2gX+vH86L1k=
|
||||
modernc.org/strutil v1.1.1/go.mod h1:DE+MQQ/hjKBZS2zNInV5hhcipt5rLPWkmpbGeW5mmdw=
|
||||
modernc.org/zappy v1.0.3/go.mod h1:w/Akq8ipfols/xZJdR5IYiQNOqC80qz2mVvsEwEbkiI=
|
263
internal/auth/auth.go
Normal file
263
internal/auth/auth.go
Normal file
@ -0,0 +1,263 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright © 2022 Roberto Hidalgo <nidito@un.rob.mx>
|
||||
package auth
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"git.rob.mx/nidito/puerta/internal/door"
|
||||
"github.com/alexedwards/scs/v2"
|
||||
"github.com/go-webauthn/webauthn/webauthn"
|
||||
"github.com/julienschmidt/httprouter"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/upper/db/v4"
|
||||
)
|
||||
|
||||
type AuthContext string
|
||||
|
||||
const (
|
||||
ContextCookieName AuthContext = "_puerta"
|
||||
ContextSessionName AuthContext = "_rex"
|
||||
ContextUserName AuthContext = "auth-username"
|
||||
ContextGreeting AuthContext = "auth-greeting"
|
||||
ContextDoor AuthContext = "auth-door"
|
||||
)
|
||||
|
||||
type Manager struct {
|
||||
db db.Session
|
||||
door door.Door
|
||||
wan *webauthn.WebAuthn
|
||||
sess *scs.SessionManager
|
||||
}
|
||||
|
||||
func NewManager(wan *webauthn.WebAuthn, door door.Door, db db.Session) *Manager {
|
||||
sessionManager := scs.New()
|
||||
sessionManager.Lifetime = 5 * time.Minute
|
||||
return &Manager{
|
||||
door: door,
|
||||
db: db,
|
||||
wan: wan,
|
||||
sess: sessionManager,
|
||||
}
|
||||
}
|
||||
|
||||
func (am *Manager) Route(router http.Handler) http.Handler {
|
||||
return am.sess.LoadAndSave(router)
|
||||
}
|
||||
|
||||
func (am *Manager) requestAuth(w http.ResponseWriter, status int) {
|
||||
http.Error(w, http.StatusText(status), status)
|
||||
}
|
||||
|
||||
func (am *Manager) NewSession(w http.ResponseWriter, req *http.Request, ps httprouter.Params) {
|
||||
err := req.ParseForm()
|
||||
if err != nil {
|
||||
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
username := req.FormValue("user")
|
||||
password := req.FormValue("password")
|
||||
|
||||
user := &User{}
|
||||
if err := am.db.Get(user, db.Cond{"name": username}); err != nil {
|
||||
err := &InvalidCredentials{code: http.StatusForbidden, reason: fmt.Sprintf("User not found for name: %s (%s)", username, err)}
|
||||
err.Log()
|
||||
http.Error(w, err.Error(), err.Code())
|
||||
return
|
||||
}
|
||||
|
||||
if err := user.Login(password); err != nil {
|
||||
code := http.StatusBadRequest
|
||||
status := http.StatusText(code)
|
||||
if err, ok := err.(InvalidCredentials); ok {
|
||||
code = err.Code()
|
||||
status = err.Error()
|
||||
err.Log()
|
||||
}
|
||||
http.Error(w, status, code)
|
||||
return
|
||||
}
|
||||
|
||||
sess, err := NewSession(user, am.db.Collection("session"))
|
||||
if err != nil {
|
||||
http.Error(w, fmt.Sprintf("Could not create a session: %s", err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Add("Set-Cookie", fmt.Sprintf("%s=%s; Max-Age=%d; Path=/;", ContextCookieName, sess.Token, user.TTL.Seconds()))
|
||||
|
||||
logrus.Infof("Created session for %s", user.Name)
|
||||
|
||||
if req.FormValue("async") == "true" {
|
||||
w.Write([]byte(user.Greeting))
|
||||
} else {
|
||||
http.Redirect(w, req, "/", http.StatusSeeOther)
|
||||
}
|
||||
}
|
||||
|
||||
func (am *Manager) Protected(handler httprouter.Handle, redirect, enforce2FA bool) httprouter.Handle {
|
||||
|
||||
return func(w http.ResponseWriter, req *http.Request, ps httprouter.Params) {
|
||||
ctx := req.Context()
|
||||
var user *User
|
||||
if ctxUser := ctx.Value(ContextUserName); ctxUser == nil {
|
||||
cookie, err := req.Cookie(string(ContextCookieName))
|
||||
|
||||
if err != nil {
|
||||
logrus.Debugf("no cookie value found in <%s>", req.Cookies())
|
||||
if redirect {
|
||||
http.Redirect(w, req, "/login", http.StatusTemporaryRedirect)
|
||||
} else {
|
||||
am.requestAuth(w, http.StatusUnauthorized)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
q := am.db.SQL().
|
||||
Select("s.token as token, ", "u.*").
|
||||
From("session as s").
|
||||
Join("user as u").On("s.user = u.id").
|
||||
Where(db.Cond{"s.token": cookie.Value})
|
||||
|
||||
session := &SessionUser{}
|
||||
if err := q.One(&session); err != nil {
|
||||
w.Header().Add("Set-Cookie", fmt.Sprintf("%s=%s; Max-Age=%d; Secure; Path=/;", ContextCookieName, "", -1))
|
||||
if redirect {
|
||||
http.Redirect(w, req, "/login", http.StatusSeeOther)
|
||||
} else {
|
||||
am.requestAuth(w, http.StatusUnauthorized)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if err := session.User.IsAllowed(time.Now()); err != nil {
|
||||
logrus.Errorf("Denying access to %s: %s", session.User.Name, err)
|
||||
am.requestAuth(w, http.StatusForbidden)
|
||||
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")
|
||||
var err error
|
||||
err = user.FetchCredentials(am.db)
|
||||
if err != nil {
|
||||
logrus.Errorf("Failed fetching credentials: %s", err.Error())
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
if len(user.credentials) == 0 {
|
||||
err = am.WebAuthnRegister(user, req)
|
||||
} else {
|
||||
err = am.WebAuthnLogin(user, req)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
if wafc, ok := err.(WebAuthFlowChallenge); ok {
|
||||
logrus.Debugf("Issuing challenge")
|
||||
w.WriteHeader(200)
|
||||
w.Header().Add("content-type", "application/json")
|
||||
w.Write([]byte(wafc.Error()))
|
||||
logrus.Debugf("Issued challenge")
|
||||
return
|
||||
}
|
||||
|
||||
logrus.Errorf("Failed during webauthn flow: %s", err.Error())
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
handler(w, req, ps)
|
||||
}
|
||||
}
|
||||
|
||||
func (am *Manager) WebAuthnRegister(user *User, req *http.Request) error {
|
||||
sd := am.sess.GetBytes(req.Context(), "wan-register")
|
||||
if sd == nil {
|
||||
logrus.Infof("Starting webauthn registration for %s", user.Name)
|
||||
options, sessionData, err := am.wan.BeginRegistration(user)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("error starting webauthn: %s", err)
|
||||
logrus.Error(err)
|
||||
return err
|
||||
}
|
||||
|
||||
var b bytes.Buffer
|
||||
if err := json.NewEncoder(&b).Encode(&sessionData); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
am.sess.Put(req.Context(), "wan-register", b.Bytes())
|
||||
|
||||
return WebAuthFlowChallenge{"register", &options}
|
||||
}
|
||||
|
||||
var sessionData webauthn.SessionData
|
||||
err := json.Unmarshal(sd, &sessionData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cred, err := am.wan.FinishRegistration(user, sessionData, req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error finishing webauthn registration: %s", err)
|
||||
}
|
||||
|
||||
data, err := json.Marshal(cred)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error encoding webauthn credential for storage: %s", err)
|
||||
}
|
||||
credential := &Credential{
|
||||
UserID: user.ID,
|
||||
Data: string(data),
|
||||
}
|
||||
|
||||
_, err = am.db.Collection("credential").Insert(credential)
|
||||
return err
|
||||
}
|
||||
|
||||
func (am *Manager) WebAuthnLogin(user *User, req *http.Request) error {
|
||||
sd := am.sess.GetBytes(req.Context(), "rex")
|
||||
if sd == nil {
|
||||
logrus.Infof("Starting webauthn login flow for %s", user.Name)
|
||||
|
||||
options, sessionData, err := am.wan.BeginLogin(user)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error starting webauthn login: %s", err)
|
||||
}
|
||||
|
||||
var b bytes.Buffer
|
||||
if err := json.NewEncoder(&b).Encode(&sessionData); err != nil {
|
||||
return fmt.Errorf("could not encode json: %s", err)
|
||||
}
|
||||
|
||||
am.sess.Put(req.Context(), "rex", b.Bytes())
|
||||
|
||||
return WebAuthFlowChallenge{"login", &options}
|
||||
}
|
||||
|
||||
var sessionData webauthn.SessionData
|
||||
err := json.Unmarshal(sd, &sessionData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = am.wan.FinishLogin(user, sessionData, req)
|
||||
return err
|
||||
}
|
||||
|
||||
func (am *Manager) Cleanup() error {
|
||||
return am.db.Collection("session").Find(db.Cond{"Expires": db.Before(time.Now())}).Delete()
|
||||
}
|
56
internal/auth/duration.go
Normal file
56
internal/auth/duration.go
Normal file
@ -0,0 +1,56 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright © 2022 Roberto Hidalgo <nidito@un.rob.mx>
|
||||
package auth
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Duration time.Duration
|
||||
|
||||
func (d Duration) MarshalDB() (any, error) {
|
||||
return time.Duration(d).String(), nil
|
||||
}
|
||||
|
||||
func (d *Duration) UnmarshalDB(value any) error {
|
||||
str := value.(string)
|
||||
suffix := str[len(str)-1]
|
||||
|
||||
if suffix == 'd' || suffix == 'w' || suffix == 'M' {
|
||||
multiplier := 1
|
||||
switch suffix {
|
||||
case 'd':
|
||||
multiplier = 24
|
||||
case 'w':
|
||||
multiplier = 24 * 7
|
||||
case 'M':
|
||||
multiplier = 24 * 7 * 30
|
||||
default:
|
||||
return fmt.Errorf("unknown suffix for time duration %s", string(suffix))
|
||||
}
|
||||
|
||||
str = str[0 : len(str)-1]
|
||||
days, err := strconv.Atoi(str)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
str = fmt.Sprintf("%dh", days*multiplier)
|
||||
}
|
||||
tmp, err := time.ParseDuration(str)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*d = Duration(tmp)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Duration) FromNow() time.Time {
|
||||
return time.Now().Add(time.Duration(*d))
|
||||
}
|
||||
|
||||
func (d *Duration) Seconds() int {
|
||||
return int(time.Duration(*d).Seconds())
|
||||
}
|
56
internal/auth/errors.go
Normal file
56
internal/auth/errors.go
Normal file
@ -0,0 +1,56 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright © 2022 Roberto Hidalgo <nidito@un.rob.mx>
|
||||
package auth
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type AuthError interface {
|
||||
Error() string
|
||||
Code() int
|
||||
Log()
|
||||
}
|
||||
|
||||
type InvalidCredentials struct {
|
||||
code int
|
||||
reason string
|
||||
}
|
||||
|
||||
func (err InvalidCredentials) Error() string {
|
||||
return "Usuario o contraseña desconocidos"
|
||||
}
|
||||
|
||||
func (err InvalidCredentials) Log() {
|
||||
logrus.Error(err.reason)
|
||||
}
|
||||
|
||||
func (err InvalidCredentials) Code() int {
|
||||
return err.code
|
||||
}
|
||||
|
||||
type WebAuthFlowChallenge struct {
|
||||
flow string
|
||||
data any
|
||||
}
|
||||
|
||||
func (c WebAuthFlowChallenge) Error() string {
|
||||
b, err := json.Marshal(map[string]any{"webauthn": c.flow, "data": c.data})
|
||||
if err != nil {
|
||||
logrus.Errorf("Could not marshal data: %s", err)
|
||||
logrus.Errorf("data: %s", c.data)
|
||||
return ""
|
||||
}
|
||||
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func (c WebAuthFlowChallenge) Log() {
|
||||
logrus.Error("responding with webauthn challenge")
|
||||
}
|
||||
|
||||
func (c WebAuthFlowChallenge) Code() int {
|
||||
return 418
|
||||
}
|
106
internal/auth/schedule.go
Normal file
106
internal/auth/schedule.go
Normal file
@ -0,0 +1,106 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright © 2022 Roberto Hidalgo <nidito@un.rob.mx>
|
||||
package auth
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func parseHour(src string) (float64, error) {
|
||||
hm := strings.Split(src, ":")
|
||||
if len(hm) == 1 {
|
||||
return strconv.ParseFloat(hm[0], 32)
|
||||
}
|
||||
|
||||
if len(hm) == 2 {
|
||||
h, err := strconv.ParseFloat(hm[0], 32)
|
||||
if err != nil {
|
||||
return 0.0, err
|
||||
}
|
||||
m, err := strconv.ParseFloat(hm[1], 32)
|
||||
if err != nil {
|
||||
return 0.0, err
|
||||
}
|
||||
return h + (m / 60.0), nil
|
||||
}
|
||||
|
||||
return 0.0, fmt.Errorf("unknown format for hour: %s", hm)
|
||||
|
||||
}
|
||||
|
||||
type UserSchedule struct {
|
||||
src string
|
||||
days []int
|
||||
hours []float64
|
||||
}
|
||||
|
||||
func (d UserSchedule) MarshalDB() ([]byte, error) {
|
||||
return json.Marshal(d.src)
|
||||
}
|
||||
|
||||
func (d *UserSchedule) UnmarshalDB(b []byte) error {
|
||||
var v string
|
||||
if err := json.Unmarshal(b, &v); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*d = UserSchedule{src: v}
|
||||
for _, kv := range strings.Split(v, " ") {
|
||||
kvSlice := strings.Split(kv, "=")
|
||||
key := kvSlice[0]
|
||||
values := strings.Split(kvSlice[1], "-")
|
||||
switch key {
|
||||
case "days":
|
||||
from, err := strconv.Atoi(values[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
until, err := strconv.Atoi(values[1])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
logrus.Infof("Parsed schedule days from: %d until %d", from, until)
|
||||
d.days = []int{from, until}
|
||||
case "hours":
|
||||
from, err := parseHour(values[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
until, err := parseHour(values[1])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
logrus.Infof("Parsed schedule hours from: %f until %f", from, until)
|
||||
d.hours = []float64{from, until}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (sch *UserSchedule) AllowedAt(t time.Time) bool {
|
||||
weekDay := int(t.Weekday())
|
||||
h, m, s := t.Clock()
|
||||
fractionalHour := float64(h) + (float64(m*60.0+s) / 3600.0)
|
||||
|
||||
logrus.Infof("Validating access at weekday %d, hour %f from rules: days=%v hours=%v at %s", weekDay, fractionalHour, sch.days, sch.hours, t.String())
|
||||
if sch.days != nil {
|
||||
if weekDay < sch.days[0] || weekDay > sch.days[1] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
if sch.hours != nil {
|
||||
if fractionalHour < sch.hours[0] || fractionalHour > sch.hours[1] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
78
internal/auth/session.go
Normal file
78
internal/auth/session.go
Normal file
@ -0,0 +1,78 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright © 2022 Roberto Hidalgo <nidito@un.rob.mx>
|
||||
package auth
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/upper/db/v4"
|
||||
)
|
||||
|
||||
var letterSrc = rand.NewSource(time.Now().UnixNano())
|
||||
|
||||
const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
const (
|
||||
// 6 bits to represent a letter index
|
||||
letterIdxBits = 6
|
||||
// All 1-bits, as many as letterIdxBits
|
||||
letterIdxMask = 1<<letterIdxBits - 1
|
||||
// # of letter indices fitting in 63 bits
|
||||
letterIdxMax = 63 / letterIdxBits
|
||||
)
|
||||
|
||||
func NewToken() string {
|
||||
sb := strings.Builder{}
|
||||
n := 32
|
||||
sb.Grow(n)
|
||||
|
||||
for i, cache, remain := n-1, letterSrc.Int63(), letterIdxMax; i >= 0; {
|
||||
if remain == 0 {
|
||||
cache, remain = letterSrc.Int63(), letterIdxMax
|
||||
}
|
||||
if idx := int(cache & letterIdxMask); idx < len(letterBytes) {
|
||||
sb.WriteByte(letterBytes[idx])
|
||||
i--
|
||||
}
|
||||
cache >>= letterIdxBits
|
||||
remain--
|
||||
}
|
||||
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
type Session struct {
|
||||
Token string `db:"token"`
|
||||
UserID int `db:"user"`
|
||||
Expires time.Time `db:"expires"`
|
||||
}
|
||||
|
||||
type SessionUser struct {
|
||||
Token string `db:"token"`
|
||||
UserID int `db:"user"`
|
||||
Expires time.Time `db:"expires"`
|
||||
User `db:",inline"`
|
||||
}
|
||||
|
||||
func (s *Session) Store(sess db.Session) db.Store {
|
||||
return sess.Collection("session")
|
||||
}
|
||||
|
||||
func (s *Session) Expired() bool {
|
||||
return s.Expires.Before(time.Now())
|
||||
}
|
||||
|
||||
func NewSession(user *User, table db.Collection) (*Session, error) {
|
||||
sess := &Session{
|
||||
Token: NewToken(),
|
||||
UserID: user.ID,
|
||||
Expires: user.TTL.FromNow(),
|
||||
}
|
||||
|
||||
// delete previous sessions
|
||||
table.Find(db.Cond{"user": user.ID}).Delete()
|
||||
// insert new one
|
||||
_, err := table.Insert(sess)
|
||||
return sess, err
|
||||
}
|
131
internal/auth/user.go
Normal file
131
internal/auth/user.go
Normal file
@ -0,0 +1,131 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright © 2022 Roberto Hidalgo <nidito@un.rob.mx>
|
||||
package auth
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/go-webauthn/webauthn/webauthn"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/upper/db/v4"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
type Credential struct {
|
||||
UserID int `db:"user"`
|
||||
Data string `db:"data"`
|
||||
wan *webauthn.Credential
|
||||
}
|
||||
|
||||
func (c *Credential) AsWebAuthn() webauthn.Credential {
|
||||
if c.wan == nil {
|
||||
c.wan = &webauthn.Credential{}
|
||||
if err := json.Unmarshal([]byte(c.Data), &c.wan); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
return *c.wan
|
||||
}
|
||||
|
||||
type User struct {
|
||||
ID int `db:"id"`
|
||||
Handle string `db:"user"`
|
||||
Name string `db:"name"`
|
||||
Password string `db:"password"`
|
||||
Schedule *UserSchedule `db:"schedule,omitempty"`
|
||||
Expires *time.Time `db:"expires,omitempty"`
|
||||
Greeting string `db:"greeting"`
|
||||
TTL Duration `db:"max_ttl"`
|
||||
Require2FA bool `db:"second_factor"`
|
||||
credentials []*Credential
|
||||
}
|
||||
|
||||
func (u *User) WebAuthnID() []byte {
|
||||
return []byte(fmt.Sprintf("%d", u.ID))
|
||||
}
|
||||
|
||||
// User Name according to the Relying Party
|
||||
func (u *User) WebAuthnName() string {
|
||||
return u.Handle
|
||||
}
|
||||
|
||||
// Display Name of the user
|
||||
func (u *User) WebAuthnDisplayName() string {
|
||||
return u.Name
|
||||
}
|
||||
|
||||
// User's icon url
|
||||
func (u *User) WebAuthnIcon() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
// Credentials owned by the user
|
||||
func (u *User) WebAuthnCredentials() []webauthn.Credential {
|
||||
res := []webauthn.Credential{}
|
||||
if u.credentials != nil {
|
||||
for _, c := range u.credentials {
|
||||
res = append(res, c.AsWebAuthn())
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func (u *User) Store(sess db.Session) db.Store {
|
||||
return sess.Collection("user")
|
||||
}
|
||||
|
||||
func (u *User) FetchCredentials(sess db.Session) error {
|
||||
creds := []*Credential{}
|
||||
err := sess.Collection("credential").Find(db.Cond{"user": u.ID}).All(&creds)
|
||||
if err != nil {
|
||||
logrus.Errorf("could not fetch credentials: %s", err)
|
||||
return err
|
||||
}
|
||||
u.credentials = creds
|
||||
logrus.Debugf("fetched %d credentials", len(creds))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o *User) UnmarshalJSON(b []byte) error {
|
||||
type alias User
|
||||
xo := &alias{TTL: Duration(30 * 24 * time.Hour)}
|
||||
if err := json.Unmarshal(b, xo); err != nil {
|
||||
return err
|
||||
}
|
||||
*o = User(*xo)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (user *User) Expired() bool {
|
||||
return user.Expires != nil && user.Expires.Before(time.Now())
|
||||
}
|
||||
|
||||
func (user *User) IsAllowed(t time.Time) error {
|
||||
if user.Expired() {
|
||||
return fmt.Errorf("usuario expirado, avísale a Roberto")
|
||||
}
|
||||
|
||||
if user.Schedule != nil && !user.Schedule.AllowedAt(time.Now()) {
|
||||
return fmt.Errorf("accesso denegado, intente nuevamente en otro momento")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (user *User) Login(password string) error {
|
||||
if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)); err != nil {
|
||||
reason := fmt.Sprintf("Incorrect password for %s", user.Name)
|
||||
return &InvalidCredentials{code: http.StatusForbidden, reason: reason}
|
||||
}
|
||||
|
||||
if user.Expired() {
|
||||
reason := fmt.Sprintf("Expired user tried to login: %s", user.Name)
|
||||
return &InvalidCredentials{code: http.StatusForbidden, reason: reason}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
109
internal/door/door.go
Normal file
109
internal/door/door.go
Normal file
@ -0,0 +1,109 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright © 2022 Roberto Hidalgo <nidito@un.rob.mx>
|
||||
package door
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
var (
|
||||
isOpening bool
|
||||
statusMu sync.Mutex
|
||||
)
|
||||
|
||||
type newDoorFunc func(map[string]any) Door
|
||||
|
||||
var adapters = &struct {
|
||||
factories map[string]newDoorFunc
|
||||
names []string
|
||||
}{
|
||||
factories: map[string]newDoorFunc{},
|
||||
names: []string{},
|
||||
}
|
||||
|
||||
func _register(name string, factory newDoorFunc) {
|
||||
adapters.factories[name] = factory
|
||||
adapters.names = append(adapters.names, name)
|
||||
}
|
||||
|
||||
type Door interface {
|
||||
IsOpen() (bool, error)
|
||||
Open(errors chan<- error, done chan<- bool)
|
||||
}
|
||||
|
||||
func setStatus(status bool) {
|
||||
statusMu.Lock()
|
||||
isOpening = status
|
||||
statusMu.Unlock()
|
||||
}
|
||||
|
||||
// RequestToEnter opens the door unless it's already open or opening
|
||||
func RequestToEnter(door Door, username string) error {
|
||||
statusMu.Lock()
|
||||
if isOpening {
|
||||
defer statusMu.Unlock()
|
||||
return &DoorCommunicationError{"checking status", fmt.Errorf("Door is busy processing another request")}
|
||||
}
|
||||
|
||||
isOpen, err := door.IsOpen()
|
||||
if err != nil {
|
||||
statusMu.Unlock()
|
||||
return &DoorCommunicationError{"checking status", err}
|
||||
} else if isOpen {
|
||||
statusMu.Unlock()
|
||||
return &DoorAlreadyOpen{}
|
||||
}
|
||||
|
||||
// okay, we're triggering an open and preventing others
|
||||
// from doing the same until this function toggles this value again
|
||||
isOpening = true
|
||||
statusMu.Unlock()
|
||||
logrus.Infof("Opening door for %s\n", username)
|
||||
|
||||
errors := make(chan error, 2)
|
||||
done := make(chan bool)
|
||||
go door.Open(errors, done)
|
||||
|
||||
if err = <-errors; err != nil {
|
||||
setStatus(false)
|
||||
return &DoorCommunicationError{"opening", err}
|
||||
}
|
||||
|
||||
logrus.Infof("Door opened for %s", username)
|
||||
|
||||
go func() {
|
||||
// Door might continue working on stuff after we Open,
|
||||
// wait for done or another error
|
||||
select {
|
||||
case <-done:
|
||||
logrus.Info("REX complete")
|
||||
case err, ok := <-errors:
|
||||
if ok && err != nil {
|
||||
logrus.Errorf("Failed during power off: %s", err)
|
||||
} else if ok {
|
||||
logrus.Info("Door power shut off correctly")
|
||||
}
|
||||
}
|
||||
// now it's safe for others to open the door
|
||||
setStatus(false)
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewDoor(config map[string]any) (Door, error) {
|
||||
adapterName, hasAdapter := config["kind"]
|
||||
if !hasAdapter {
|
||||
return nil, fmt.Errorf("missing DOOR_ADAPTER")
|
||||
}
|
||||
|
||||
factory, exists := adapters.factories[adapterName.(string)]
|
||||
if !exists {
|
||||
return nil, fmt.Errorf("unknown DOOR_ADAPTER \"%s\", not one of [%s]", adapterName, strings.Join(adapters.names, ","))
|
||||
}
|
||||
|
||||
return factory(config), nil
|
||||
}
|
31
internal/door/errors.go
Normal file
31
internal/door/errors.go
Normal file
@ -0,0 +1,31 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright © 2022 Roberto Hidalgo <nidito@un.rob.mx>
|
||||
package door
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type DoorCommunicationError struct {
|
||||
during string
|
||||
err error
|
||||
}
|
||||
|
||||
func (err *DoorCommunicationError) Error() string {
|
||||
return fmt.Sprintf("ould not get door status while %s: %s", err.during, err.err.Error())
|
||||
}
|
||||
|
||||
func (err *DoorCommunicationError) Code() int {
|
||||
return http.StatusInternalServerError
|
||||
}
|
||||
|
||||
type DoorAlreadyOpen struct{}
|
||||
|
||||
func (err *DoorAlreadyOpen) Error() string {
|
||||
return "door is already open"
|
||||
}
|
||||
|
||||
func (err *DoorAlreadyOpen) Code() int {
|
||||
return http.StatusPreconditionFailed
|
||||
}
|
133
internal/door/hue.go
Normal file
133
internal/door/hue.go
Normal file
@ -0,0 +1,133 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright © 2022 Roberto Hidalgo <nidito@un.rob.mx>
|
||||
package door
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/amimof/huego"
|
||||
hue "github.com/amimof/huego"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type HueConfig struct {
|
||||
ip string
|
||||
username string
|
||||
device int
|
||||
}
|
||||
|
||||
type Hue struct {
|
||||
bridge *hue.Bridge
|
||||
device *hue.Light
|
||||
config *HueConfig
|
||||
}
|
||||
|
||||
func init() {
|
||||
_register("hue", NewHue)
|
||||
}
|
||||
|
||||
func NewHue(config map[string]any) Door {
|
||||
|
||||
cfg := &HueConfig{
|
||||
ip: config["ip"].(string),
|
||||
username: config["username"].(string),
|
||||
device: -1,
|
||||
}
|
||||
if config["device"] != nil {
|
||||
cfg.device = config["device"].(int)
|
||||
}
|
||||
|
||||
h := &Hue{
|
||||
bridge: huego.New(cfg.ip, cfg.username),
|
||||
config: cfg,
|
||||
}
|
||||
|
||||
logrus.Infof("Hue client for %s starting", cfg.ip)
|
||||
|
||||
if cfg.username != "" && cfg.device > -1 {
|
||||
device, err := h.bridge.GetLight(cfg.device)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
h.device = device
|
||||
}
|
||||
return h
|
||||
}
|
||||
|
||||
func (h *Hue) Setup(domain string) error {
|
||||
if h.config.username == "" {
|
||||
logrus.Info("Pairing with bridge, please press the button")
|
||||
user, err := h.bridge.CreateUser(domain)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
logrus.Infof("Created user id: %s", user)
|
||||
h.bridge = h.bridge.Login(user)
|
||||
}
|
||||
|
||||
if h.config.device == -1 {
|
||||
logrus.Info("Looking for devices...")
|
||||
|
||||
lights, err := h.bridge.GetLights()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, l := range lights {
|
||||
if l.Type == "On/Off plug-in unit" {
|
||||
logrus.Infof("Found %s named %s with ID: %d", l.ProductName, l.Name, l.ID)
|
||||
} else {
|
||||
logrus.Debugf("Found %s (%s) named %s with ID: %d", l.Type, l.ProductName, l.Name, l.ID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
logrus.Info("Setup complete")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *Hue) IsOpen() (bool, error) {
|
||||
return h.device.IsOn(), nil
|
||||
}
|
||||
|
||||
func (h *Hue) Open(errors chan<- error, done chan<- bool) {
|
||||
defer close(errors)
|
||||
defer close(done)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
err := h.device.SetStateContext(ctx, hue.State{On: true})
|
||||
|
||||
if err != nil {
|
||||
errors <- err
|
||||
return
|
||||
}
|
||||
|
||||
errors <- nil
|
||||
|
||||
time.Sleep(4 * time.Second)
|
||||
|
||||
ctx, cancel = context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
err = h.device.SetStateContext(ctx, hue.State{On: false})
|
||||
|
||||
if err != nil {
|
||||
errors <- err
|
||||
return
|
||||
}
|
||||
|
||||
done <- true
|
||||
}
|
||||
|
||||
func (h *Hue) Close(errors chan error) {
|
||||
err, ok := <-errors
|
||||
if ok && err != nil {
|
||||
logrus.Errorf("Failed during power off: %s", err)
|
||||
return
|
||||
} else if ok {
|
||||
logrus.Info("Door power shut off correctly")
|
||||
}
|
||||
}
|
51
internal/door/mock.go
Normal file
51
internal/door/mock.go
Normal file
@ -0,0 +1,51 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright © 2022 Roberto Hidalgo <nidito@un.rob.mx>
|
||||
package door
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func init() {
|
||||
_register("dry-run", NewMock)
|
||||
}
|
||||
|
||||
type mockDoor struct {
|
||||
Status bool
|
||||
FailedToOpen error
|
||||
FailedToClose error
|
||||
}
|
||||
|
||||
func NewMock(config map[string]any) Door {
|
||||
logrus.Info("Initializing mock client")
|
||||
return &mockDoor{
|
||||
Status: false,
|
||||
}
|
||||
}
|
||||
|
||||
func (md *mockDoor) IsOpen() (bool, error) {
|
||||
return md.Status, nil
|
||||
}
|
||||
|
||||
func (md *mockDoor) Open(errors chan<- error, done chan<- bool) {
|
||||
defer close(errors)
|
||||
if md.FailedToOpen != nil {
|
||||
errors <- md.FailedToOpen
|
||||
return
|
||||
}
|
||||
|
||||
md.Status = true
|
||||
errors <- nil
|
||||
|
||||
time.Sleep(4 * time.Second)
|
||||
md.Status = false
|
||||
|
||||
if md.FailedToClose != nil {
|
||||
errors <- md.FailedToClose
|
||||
return
|
||||
}
|
||||
|
||||
done <- true
|
||||
}
|
135
internal/door/wemo.go
Normal file
135
internal/door/wemo.go
Normal file
@ -0,0 +1,135 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright © 2022 Roberto Hidalgo <nidito@un.rob.mx>
|
||||
package door
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func init() {
|
||||
_register("wemo", NewWemo)
|
||||
}
|
||||
|
||||
type Wemo struct {
|
||||
endpoint string
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
func NewWemo(config map[string]any) Door {
|
||||
logrus.Infof("Wemo client for %s starting", config["endpoint"])
|
||||
return &Wemo{
|
||||
endpoint: config["endpoint"].(string),
|
||||
client: &http.Client{Timeout: 4 * time.Second},
|
||||
}
|
||||
}
|
||||
|
||||
const wemoBodyGet string = `<?xml version="1.0" encoding="utf-8"?>
|
||||
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
|
||||
<s:Body>
|
||||
<u:GetBinaryState xmlns:u="urn:Belkin:service:basicevent:1">
|
||||
</u:GetBinaryState>
|
||||
</s:Body>
|
||||
</s:Envelope>`
|
||||
|
||||
const wemoBodySetTemplate string = `<?xml version="1.0" encoding="utf-8"?>
|
||||
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
|
||||
<s:Body>
|
||||
<u:SetBinaryState xmlns:u="urn:Belkin:service:basicevent:1">
|
||||
<BinaryState>%s</BinaryState>
|
||||
</u:SetBinaryState>
|
||||
</s:Body>
|
||||
</s:Envelope>`
|
||||
|
||||
func (wm *Wemo) request(op string, xml string) (string, error) {
|
||||
logrus.Debugf("requesting %s with body len %d\n", op, len(xml))
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
body := bytes.NewBufferString(xml)
|
||||
url := fmt.Sprintf("http://%s:49153/upnp/control/basicevent1", wm.endpoint)
|
||||
req, err := http.NewRequestWithContext(ctx, "POST", url, body)
|
||||
if err != nil {
|
||||
logrus.Errorf("Failed creating http request to wemo: %s", err)
|
||||
return "", err
|
||||
}
|
||||
|
||||
opHeader := fmt.Sprintf(`"urn:Belkin:service:basicevent:1#%s"`, op)
|
||||
req.Header.Set("content-type", `text/xml; charset="utf-8"`)
|
||||
req.Header.Set("content-length", fmt.Sprintf("%d", req.ContentLength))
|
||||
req.Header.Set("user-agent", "puerta.nidi.to")
|
||||
req.Header.Set("accept", "*/*")
|
||||
req.Header.Set("soapaction", opHeader)
|
||||
|
||||
dump, _ := httputil.DumpRequest(req, true)
|
||||
logrus.Debugf("%s\n%s", string(dump), xml)
|
||||
|
||||
res, err := wm.client.Do(req)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if res.StatusCode > 299 {
|
||||
return "", fmt.Errorf("%s Request failed with code %d", op, res.StatusCode)
|
||||
}
|
||||
|
||||
defer res.Body.Close()
|
||||
bodyBytes, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(bodyBytes), err
|
||||
}
|
||||
|
||||
func (wm *Wemo) IsOpen() (bool, error) {
|
||||
statusBody, err := wm.request("GetBinaryState", wemoBodyGet)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if strings.Contains(statusBody, "<BinaryState>0</BinaryState>") {
|
||||
return false, nil
|
||||
} else if strings.Contains(statusBody, "<BinaryState>1</BinaryState>") {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
return false, fmt.Errorf("unknown response from wemo: %s", statusBody)
|
||||
}
|
||||
|
||||
func (wm *Wemo) Open(errors chan<- error, done chan<- bool) {
|
||||
defer close(errors)
|
||||
defer close(done)
|
||||
if _, err := wm.request("SetBinaryState", fmt.Sprintf(wemoBodySetTemplate, "1")); err != nil {
|
||||
errors <- err
|
||||
return
|
||||
}
|
||||
|
||||
errors <- nil
|
||||
|
||||
time.Sleep(4 * time.Second)
|
||||
|
||||
if _, err := wm.request("SetBinaryState", fmt.Sprintf(wemoBodySetTemplate, "0")); err != nil {
|
||||
errors <- err
|
||||
return
|
||||
}
|
||||
|
||||
done <- true
|
||||
}
|
||||
|
||||
func (wm *Wemo) Close(errors chan error) {
|
||||
err, ok := <-errors
|
||||
if ok && err != nil {
|
||||
logrus.Errorf("Failed during power off: %s", err)
|
||||
return
|
||||
} else if ok {
|
||||
logrus.Info("Door power shut off correctly")
|
||||
}
|
||||
}
|
15
internal/errors/errors.go
Normal file
15
internal/errors/errors.go
Normal file
@ -0,0 +1,15 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright © 2022 Roberto Hidalgo <nidito@un.rob.mx>
|
||||
package errors
|
||||
|
||||
type HTTPError interface {
|
||||
Error() string
|
||||
Code() int
|
||||
}
|
||||
|
||||
func ToHTTP(err error) (string, int) {
|
||||
if err := err.(HTTPError); err != nil {
|
||||
return err.Error(), err.Code()
|
||||
}
|
||||
return err.Error(), 500
|
||||
}
|
28
internal/server/index.html
Normal file
28
internal/server/index.html
Normal file
@ -0,0 +1,28 @@
|
||||
<!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" />
|
||||
<script src="https://cdn.jsdelivr.net/npm/@teamhanko/hanko-webauthn@latest/dist/browser-global/hanko-webauthn.browser-global.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<header id="main-header">
|
||||
<div class="container">
|
||||
<h1>Puerta</h1>
|
||||
<p>Ábrete sésamo</p>
|
||||
</div>
|
||||
</header>
|
||||
<main class="container">
|
||||
<form id="open" method="post" action="/open">
|
||||
<button id="rex">Abrir</button>
|
||||
</form>
|
||||
</main>
|
||||
<script src="/static/index.js" async="async"></script>
|
||||
</body>
|
||||
</html>
|
33
internal/server/login.html
Normal file
33
internal/server/login.html
Normal file
@ -0,0 +1,33 @@
|
||||
<!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>Ábrete sésamo</p>
|
||||
</div>
|
||||
</header>
|
||||
<main class="container">
|
||||
<form id="login" method="post" action="/api/login">
|
||||
<span class="error"></span>
|
||||
<label for="user">Usuario</label>
|
||||
<input type="text" name="user" autocorrect="false" />
|
||||
|
||||
<label for="password">Password</label>
|
||||
<input type="password" name="password" />
|
||||
<button id="auth" type="submit">Iniciar Sesión</button>
|
||||
</form>
|
||||
</main>
|
||||
<script src="/static/login.js" async="async"></script>
|
||||
</body>
|
||||
</html>
|
134
internal/server/server.go
Normal file
134
internal/server/server.go
Normal file
@ -0,0 +1,134 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright © 2022 Roberto Hidalgo <nidito@un.rob.mx>
|
||||
package server
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"git.rob.mx/nidito/puerta/internal/auth"
|
||||
"git.rob.mx/nidito/puerta/internal/door"
|
||||
"git.rob.mx/nidito/puerta/internal/errors"
|
||||
"github.com/go-webauthn/webauthn/webauthn"
|
||||
"github.com/julienschmidt/httprouter"
|
||||
"github.com/upper/db/v4"
|
||||
"github.com/upper/db/v4/adapter/sqlite"
|
||||
)
|
||||
|
||||
//go:embed login.html
|
||||
var loginTemplate []byte
|
||||
|
||||
//go:embed index.html
|
||||
var indexTemplate []byte
|
||||
|
||||
//go:embed static/*
|
||||
var staticFiles embed.FS
|
||||
|
||||
type HTTPConfig struct {
|
||||
Listen int `yaml:"listen"`
|
||||
Domain string `yaml:"domain"`
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
Name string `yaml:"name"`
|
||||
Adapter map[string]any `yaml:"adapter"`
|
||||
HTTP *HTTPConfig `yaml:"http"`
|
||||
|
||||
DB string `yaml:"db"`
|
||||
}
|
||||
|
||||
func ConfigDefaults(dbPath string) *Config {
|
||||
return &Config{
|
||||
DB: dbPath,
|
||||
HTTP: &HTTPConfig{
|
||||
Listen: 8000,
|
||||
Domain: "localhost",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func CORS(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Header.Get("Access-Control-Request-Method") != "" {
|
||||
// Set CORS headers
|
||||
header := w.Header()
|
||||
header.Set("Access-Control-Allow-Methods", r.Header.Get("Allow"))
|
||||
header.Set("Access-Control-Allow-Origin", "")
|
||||
}
|
||||
|
||||
// Adjust status code to 204
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func rex(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
|
||||
userName := r.Context().Value(auth.ContextUserName).(string)
|
||||
|
||||
if err := door.RequestToEnter(_door, userName); err != nil {
|
||||
message, code := errors.ToHTTP(err)
|
||||
http.Error(w, message, code)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Fprintf(w, `{"status": "ok"}`)
|
||||
}
|
||||
|
||||
var _db db.Session
|
||||
var _door door.Door
|
||||
|
||||
func Initialize(config *Config) (http.Handler, error) {
|
||||
router := httprouter.New()
|
||||
router.GlobalOPTIONS = http.HandlerFunc(CORS)
|
||||
|
||||
db := sqlite.ConnectionURL{
|
||||
Database: config.DB,
|
||||
Options: map[string]string{
|
||||
"_journal": "WAL",
|
||||
"_busy_timeout": "5000",
|
||||
},
|
||||
}
|
||||
var err error
|
||||
_db, err = sqlite.Open(db)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_door, err = door.NewDoor(config.Adapter)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
uri := fmt.Sprintf("http://%s:%d", config.HTTP.Domain, config.HTTP.Listen)
|
||||
|
||||
wan, err := webauthn.New(&webauthn.Config{
|
||||
RPDisplayName: config.Name,
|
||||
RPID: config.HTTP.Domain,
|
||||
RPOrigins: []string{uri},
|
||||
// RPIcon: "https://go-webauthn.local/logo.png",
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
am := auth.NewManager(wan, _door, _db)
|
||||
|
||||
serverRoot, err := fs.Sub(staticFiles, "static")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
router.ServeFiles("/static/*filepath", http.FS(serverRoot))
|
||||
router.GET("/login", renderTemplate(loginTemplate))
|
||||
router.GET("/", am.Protected(renderTemplate(indexTemplate), true, false))
|
||||
router.POST("/api/login", am.NewSession)
|
||||
router.POST("/api/rex", am.Protected(rex, false, true))
|
||||
|
||||
return am.Route(router), nil
|
||||
}
|
||||
|
||||
func renderTemplate(template []byte) httprouter.Handle {
|
||||
return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
|
||||
w.Write(template)
|
||||
}
|
||||
}
|
74
internal/server/static/index.css
Normal file
74
internal/server/static/index.css
Normal file
@ -0,0 +1,74 @@
|
||||
button {
|
||||
background: rgba(255,255,255,.6);
|
||||
font-family: "Aestetico", sans-serif;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
display: block;
|
||||
color: #c11145;
|
||||
border: 5px solid #c11145;
|
||||
transition: all ease-in-out .5s;
|
||||
}
|
||||
|
||||
#rex {
|
||||
font-size: 5em;
|
||||
border-radius: 100%;
|
||||
width: 75vw;
|
||||
height: 75vw;
|
||||
margin: .5em auto;
|
||||
max-width: 50vh;
|
||||
max-height: 50vh;
|
||||
border-width: 10px;
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
#auth {
|
||||
font-size: 1.6em;
|
||||
padding: .4em 1em;
|
||||
margin: 1em 0;
|
||||
width: 100%;
|
||||
max-width: 50vw;
|
||||
}
|
||||
|
||||
button[disabled] {
|
||||
filter:saturate(0);
|
||||
}
|
||||
|
||||
#open.success button{
|
||||
color: rgb(27, 163, 0);
|
||||
border-color: rgb(27, 163, 0)
|
||||
}
|
||||
|
||||
#open.requested button {
|
||||
color: rgb(0, 76, 163);
|
||||
border-color: rgb(0, 76, 163);
|
||||
}
|
||||
|
||||
#open.failed button {
|
||||
color: #fff;
|
||||
background-color: rgb(175, 39, 39);
|
||||
border-color: rgb(126, 26, 26);
|
||||
}
|
||||
|
||||
form {
|
||||
display: block;
|
||||
}
|
||||
|
||||
label {
|
||||
font-family: "Aestetico", sans-serif;
|
||||
font-size: 1.2em;
|
||||
line-height: 1.8em;
|
||||
}
|
||||
|
||||
input {
|
||||
display: block;
|
||||
font-family: "Fira Code", monospace;
|
||||
font-size: 1.5em;
|
||||
width: 100%;
|
||||
max-width: 50vw;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
input {
|
||||
max-width: auto;
|
||||
}
|
||||
}
|
126
internal/server/static/index.js
Normal file
126
internal/server/static/index.js
Normal file
@ -0,0 +1,126 @@
|
||||
const button = document.querySelector("#open button")
|
||||
const form = document.querySelector("#open")
|
||||
const { create: createCredentials, get: getCredentials } = hankoWebAuthn;
|
||||
|
||||
async function RequestToEnter() {
|
||||
console.debug("requesting to enter")
|
||||
let response = await window.fetch(`/api/rex`, {
|
||||
method: 'POST',
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
let message = response.statusText
|
||||
try {
|
||||
let json = await response.json()
|
||||
if (json.message) {
|
||||
message = `${message}: ${json.message}`
|
||||
}
|
||||
} catch {}
|
||||
|
||||
throw new Error(message);
|
||||
}
|
||||
|
||||
let json = {}
|
||||
try {
|
||||
json = await response.json()
|
||||
} catch {}
|
||||
|
||||
if (json.webauthn) {
|
||||
try {
|
||||
if (json.webauthn == "register") {
|
||||
await register(json.data)
|
||||
} else if (json.webauthn == "login"){
|
||||
await login(json.data)
|
||||
}
|
||||
} catch(err) {
|
||||
console.error("webauthn failure", err)
|
||||
}
|
||||
} else if (json.status == "ok") {
|
||||
console.debug("Door opened")
|
||||
}
|
||||
|
||||
return response.status
|
||||
}
|
||||
|
||||
async function register(data) {
|
||||
console.debug("creating credentials")
|
||||
const credential = await createCredentials(data);
|
||||
|
||||
console.debug(`exchanging credential: ${JSON.stringify(credential)}`)
|
||||
let response = await window.fetch(`/api/rex`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(credential)
|
||||
})
|
||||
|
||||
console.debug("sent credential creation request")
|
||||
|
||||
if (!response.ok) {
|
||||
let message = response.statusText
|
||||
try {
|
||||
let json = await response.json()
|
||||
if (json.message) {
|
||||
message = `${message}: ${json.message}`
|
||||
}
|
||||
} catch {}
|
||||
|
||||
throw new Error(message);
|
||||
}
|
||||
}
|
||||
|
||||
async function login(data) {
|
||||
console.debug("fetching passkey")
|
||||
const credential = await getCredentials(data);
|
||||
|
||||
console.debug(`exchanging credential: ${JSON.stringify(credential)}`)
|
||||
let response = await window.fetch(`/api/rex`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(credential)
|
||||
})
|
||||
|
||||
console.debug("sent passkey")
|
||||
|
||||
if (!response.ok) {
|
||||
let message = response.statusText
|
||||
try {
|
||||
let json = await response.json()
|
||||
if (json.message) {
|
||||
message = `${message}: ${json.message}`
|
||||
}
|
||||
} catch {}
|
||||
|
||||
throw new Error(message);
|
||||
}
|
||||
}
|
||||
|
||||
function clearStatus() {
|
||||
form.classList.remove("failed")
|
||||
form.classList.remove("success")
|
||||
}
|
||||
|
||||
button.addEventListener("click", function(evt){
|
||||
evt.preventDefault()
|
||||
button.disabled = true
|
||||
|
||||
clearStatus()
|
||||
|
||||
RequestToEnter().then(() => {
|
||||
form.classList.add("success")
|
||||
}).catch((err) => {
|
||||
form.classList.add("failed")
|
||||
console.error(`Error: ${err}`)
|
||||
}).finally(() => {
|
||||
form.classList.remove("requested")
|
||||
button.disabled = false
|
||||
setTimeout(clearStatus, 5000)
|
||||
})
|
||||
|
||||
return false
|
||||
})
|
50
internal/server/static/login.js
Normal file
50
internal/server/static/login.js
Normal file
@ -0,0 +1,50 @@
|
||||
const button = document.querySelector("#auth")
|
||||
const form = document.querySelector("#login")
|
||||
|
||||
async function Login() {
|
||||
const response = await window.fetch(`/api/login`, {
|
||||
method: 'POST',
|
||||
body: new URLSearchParams(new FormData(form)),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
let message = response.statusText
|
||||
try {
|
||||
message = await response.text()
|
||||
} catch {}
|
||||
|
||||
throw new Error(message);
|
||||
}
|
||||
|
||||
return response.status
|
||||
}
|
||||
|
||||
function clearStatus() {
|
||||
form.classList.remove("failed")
|
||||
form.classList.remove("success")
|
||||
}
|
||||
|
||||
function submit(evt){
|
||||
evt.preventDefault()
|
||||
button.disabled = true
|
||||
|
||||
document.querySelector('.error').innerText = ""
|
||||
clearStatus()
|
||||
|
||||
Login().then(() => {
|
||||
window.location = "/";
|
||||
}).catch((err) => {
|
||||
form.classList.add("failed")
|
||||
document.querySelector('.error').innerText = err
|
||||
console.error(err)
|
||||
}).finally(() => {
|
||||
form.classList.remove("requested")
|
||||
button.disabled = false
|
||||
setTimeout(clearStatus, 5000)
|
||||
})
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
button.addEventListener("click", submit)
|
||||
form.addEventListener("submit", submit)
|
29
internal/server/static/serviceworker.js
Normal file
29
internal/server/static/serviceworker.js
Normal file
@ -0,0 +1,29 @@
|
||||
importScripts(
|
||||
'https://storage.googleapis.com/workbox-cdn/releases/6.4.1/workbox-sw.js'
|
||||
);
|
||||
|
||||
workbox.loadModule('workbox-strategies');
|
||||
|
||||
self.addEventListener("install", event => {
|
||||
console.log("Service worker installed");
|
||||
|
||||
const urlsToCache = ["/", "app.js", "styles.css", "logo.svg"];
|
||||
event.waitUntil(
|
||||
caches.open("pwa-assets")
|
||||
.then(cache => {
|
||||
return cache.addAll(urlsToCache);
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
self.addEventListener("activate", event => {
|
||||
console.log("Service worker activated");
|
||||
});
|
||||
|
||||
|
||||
self.addEventListener('fetch', event => {
|
||||
if (event.request.url.endsWith('.png')) {
|
||||
const cacheFirst = new workbox.strategies.CacheFirst();
|
||||
event.respondWith(cacheFirst.handle({request: event.request}));
|
||||
}
|
||||
});
|
39
main.go
Normal file
39
main.go
Normal file
@ -0,0 +1,39 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright © 2022 Roberto Hidalgo <nidito@un.rob.mx>
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"git.rob.mx/nidito/chinampa"
|
||||
"git.rob.mx/nidito/chinampa/pkg/runtime"
|
||||
_ "git.rob.mx/nidito/puerta/cmd/admin"
|
||||
_ "git.rob.mx/nidito/puerta/cmd/hue"
|
||||
_ "git.rob.mx/nidito/puerta/cmd/server"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func main() {
|
||||
logrus.SetFormatter(&logrus.TextFormatter{
|
||||
DisableLevelTruncation: true,
|
||||
DisableTimestamp: true,
|
||||
ForceColors: runtime.ColorEnabled(),
|
||||
})
|
||||
|
||||
if runtime.DebugEnabled() {
|
||||
logrus.SetLevel(logrus.DebugLevel)
|
||||
logrus.Debug("Debugging enabled")
|
||||
}
|
||||
|
||||
cfg := chinampa.Config{
|
||||
Name: "puerta",
|
||||
Version: "0.0.0",
|
||||
Summary: "opens the door to my house",
|
||||
Description: "Does other door related stuff too.",
|
||||
}
|
||||
|
||||
if err := chinampa.Execute(cfg); err != nil {
|
||||
logrus.Errorf("total failure: %s", err)
|
||||
os.Exit(2)
|
||||
}
|
||||
}
|
41
schema.sql
Normal file
41
schema.sql
Normal file
@ -0,0 +1,41 @@
|
||||
|
||||
CREATE TABLE user(
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name VARCHAR(255) NOT NULL UNIQUE,
|
||||
password TEXT,
|
||||
expires TEXT, -- datetime
|
||||
greeting TEXT,
|
||||
max_ttl TEXT DEFAULT "30d", -- golang auth.Duration
|
||||
second_factor BOOLEAN DEFAULT 1,
|
||||
schedule TEXT -- golang auth.Schedule
|
||||
);
|
||||
|
||||
CREATE INDEX user_id ON user(id);
|
||||
CREATE INDEX user_name ON user(name);
|
||||
|
||||
CREATE TABLE credential(
|
||||
user INTEGER NOT NULL,
|
||||
data text NOT NULL,
|
||||
FOREIGN KEY(user) REFERENCES user(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX credential_user ON credential(id);
|
||||
|
||||
|
||||
CREATE TABLE session(
|
||||
token TEXT PRIMARY KEY,
|
||||
user INTEGER NOT NULL,
|
||||
expires TEXT NOT NULL, -- datetime
|
||||
FOREIGN KEY(user) REFERENCES user(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX session_token ON session(token);
|
||||
|
||||
|
||||
CREATE TABLE sessions (
|
||||
token TEXT PRIMARY KEY,
|
||||
data BLOB NOT NULL,
|
||||
expiry REAL NOT NULL
|
||||
);
|
||||
|
||||
CREATE INDEX sessions_expiry_idx ON sessions(expiry);
|
Loading…
Reference in New Issue
Block a user