From e20c05bc74359d66a6a6d1e8f977918fc82967f1 Mon Sep 17 00:00:00 2001 From: Roberto Hidalgo Date: Fri, 30 Dec 2022 00:51:51 -0600 Subject: [PATCH] knock knock --- .gitignore | 1 + LICENSE.txt | 201 ++++++++++ README.md | 0 cmd/admin/user.go | 145 +++++++ cmd/hue/main.go | 85 +++++ cmd/server/main.go | 61 +++ config.template.yaml | 13 + go.mod | 56 +++ go.sum | 482 ++++++++++++++++++++++++ internal/auth/auth.go | 263 +++++++++++++ internal/auth/duration.go | 56 +++ internal/auth/errors.go | 56 +++ internal/auth/schedule.go | 106 ++++++ internal/auth/session.go | 78 ++++ internal/auth/user.go | 131 +++++++ internal/door/door.go | 109 ++++++ internal/door/errors.go | 31 ++ internal/door/hue.go | 133 +++++++ internal/door/mock.go | 51 +++ internal/door/wemo.go | 135 +++++++ internal/errors/errors.go | 15 + internal/server/index.html | 28 ++ internal/server/login.html | 33 ++ internal/server/server.go | 134 +++++++ internal/server/static/index.css | 74 ++++ internal/server/static/index.js | 126 +++++++ internal/server/static/login.js | 50 +++ internal/server/static/serviceworker.js | 29 ++ main.go | 39 ++ schema.sql | 41 ++ 30 files changed, 2762 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE.txt create mode 100644 README.md create mode 100644 cmd/admin/user.go create mode 100644 cmd/hue/main.go create mode 100644 cmd/server/main.go create mode 100644 config.template.yaml create mode 100644 go.mod create mode 100644 go.sum create mode 100644 internal/auth/auth.go create mode 100644 internal/auth/duration.go create mode 100644 internal/auth/errors.go create mode 100644 internal/auth/schedule.go create mode 100644 internal/auth/session.go create mode 100644 internal/auth/user.go create mode 100644 internal/door/door.go create mode 100644 internal/door/errors.go create mode 100644 internal/door/hue.go create mode 100644 internal/door/mock.go create mode 100644 internal/door/wemo.go create mode 100644 internal/errors/errors.go create mode 100644 internal/server/index.html create mode 100644 internal/server/login.html create mode 100644 internal/server/server.go create mode 100644 internal/server/static/index.css create mode 100644 internal/server/static/index.js create mode 100644 internal/server/static/login.js create mode 100644 internal/server/static/serviceworker.js create mode 100644 main.go create mode 100644 schema.sql diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..267d12d --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +puerta diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/LICENSE.txt @@ -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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..e69de29 diff --git a/cmd/admin/user.go b/cmd/admin/user.go new file mode 100644 index 0000000..1765540 --- /dev/null +++ b/cmd/admin/user.go @@ -0,0 +1,145 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright © 2022 Roberto Hidalgo +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 + + }, +} diff --git a/cmd/hue/main.go b/cmd/hue/main.go new file mode 100644 index 0000000..31cac29 --- /dev/null +++ b/cmd/hue/main.go @@ -0,0 +1,85 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright © 2022 Roberto Hidalgo +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") + }, +} diff --git a/cmd/server/main.go b/cmd/server/main.go new file mode 100644 index 0000000..15c6464 --- /dev/null +++ b/cmd/server/main.go @@ -0,0 +1,61 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright © 2022 Roberto Hidalgo +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) + }, +} diff --git a/config.template.yaml b/config.template.yaml new file mode 100644 index 0000000..93cd8bb --- /dev/null +++ b/config.template.yaml @@ -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 \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..14efe82 --- /dev/null +++ b/go.mod @@ -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 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..71e3874 --- /dev/null +++ b/go.sum @@ -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= diff --git a/internal/auth/auth.go b/internal/auth/auth.go new file mode 100644 index 0000000..11d5821 --- /dev/null +++ b/internal/auth/auth.go @@ -0,0 +1,263 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright © 2022 Roberto Hidalgo +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() +} diff --git a/internal/auth/duration.go b/internal/auth/duration.go new file mode 100644 index 0000000..eef81cc --- /dev/null +++ b/internal/auth/duration.go @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright © 2022 Roberto Hidalgo +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()) +} diff --git a/internal/auth/errors.go b/internal/auth/errors.go new file mode 100644 index 0000000..112e991 --- /dev/null +++ b/internal/auth/errors.go @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright © 2022 Roberto Hidalgo +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 +} diff --git a/internal/auth/schedule.go b/internal/auth/schedule.go new file mode 100644 index 0000000..794209c --- /dev/null +++ b/internal/auth/schedule.go @@ -0,0 +1,106 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright © 2022 Roberto Hidalgo +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 +} diff --git a/internal/auth/session.go b/internal/auth/session.go new file mode 100644 index 0000000..72b3661 --- /dev/null +++ b/internal/auth/session.go @@ -0,0 +1,78 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright © 2022 Roberto Hidalgo +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<= 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 +} diff --git a/internal/auth/user.go b/internal/auth/user.go new file mode 100644 index 0000000..1ac27a6 --- /dev/null +++ b/internal/auth/user.go @@ -0,0 +1,131 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright © 2022 Roberto Hidalgo +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 +} diff --git a/internal/door/door.go b/internal/door/door.go new file mode 100644 index 0000000..f5ad892 --- /dev/null +++ b/internal/door/door.go @@ -0,0 +1,109 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright © 2022 Roberto Hidalgo +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 +} diff --git a/internal/door/errors.go b/internal/door/errors.go new file mode 100644 index 0000000..0a5f339 --- /dev/null +++ b/internal/door/errors.go @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright © 2022 Roberto Hidalgo +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 +} diff --git a/internal/door/hue.go b/internal/door/hue.go new file mode 100644 index 0000000..0b07f17 --- /dev/null +++ b/internal/door/hue.go @@ -0,0 +1,133 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright © 2022 Roberto Hidalgo +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") + } +} diff --git a/internal/door/mock.go b/internal/door/mock.go new file mode 100644 index 0000000..b650cd4 --- /dev/null +++ b/internal/door/mock.go @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright © 2022 Roberto Hidalgo +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 +} diff --git a/internal/door/wemo.go b/internal/door/wemo.go new file mode 100644 index 0000000..d58976f --- /dev/null +++ b/internal/door/wemo.go @@ -0,0 +1,135 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright © 2022 Roberto Hidalgo +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 = ` + + + + + +` + +const wemoBodySetTemplate string = ` + + + + %s + + +` + +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, "0") { + return false, nil + } else if strings.Contains(statusBody, "1") { + 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") + } +} diff --git a/internal/errors/errors.go b/internal/errors/errors.go new file mode 100644 index 0000000..e78b035 --- /dev/null +++ b/internal/errors/errors.go @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright © 2022 Roberto Hidalgo +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 +} diff --git a/internal/server/index.html b/internal/server/index.html new file mode 100644 index 0000000..5564f8a --- /dev/null +++ b/internal/server/index.html @@ -0,0 +1,28 @@ + + + + + + + + puerta@nidi.to + + + + + + +
+
+

Puerta

+

Ábrete sésamo

+
+
+
+
+ +
+
+ + + diff --git a/internal/server/login.html b/internal/server/login.html new file mode 100644 index 0000000..89b01cb --- /dev/null +++ b/internal/server/login.html @@ -0,0 +1,33 @@ + + + + + + + + puerta@nidi.to + + + + + +
+
+

Puerta

+

Ábrete sésamo

+
+
+
+
+ + + + + + + +
+
+ + + diff --git a/internal/server/server.go b/internal/server/server.go new file mode 100644 index 0000000..ca4893c --- /dev/null +++ b/internal/server/server.go @@ -0,0 +1,134 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright © 2022 Roberto Hidalgo +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) + } +} diff --git a/internal/server/static/index.css b/internal/server/static/index.css new file mode 100644 index 0000000..6d11fd4 --- /dev/null +++ b/internal/server/static/index.css @@ -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; + } +} diff --git a/internal/server/static/index.js b/internal/server/static/index.js new file mode 100644 index 0000000..c69b4e0 --- /dev/null +++ b/internal/server/static/index.js @@ -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 +}) diff --git a/internal/server/static/login.js b/internal/server/static/login.js new file mode 100644 index 0000000..c00e0db --- /dev/null +++ b/internal/server/static/login.js @@ -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) diff --git a/internal/server/static/serviceworker.js b/internal/server/static/serviceworker.js new file mode 100644 index 0000000..12f28b3 --- /dev/null +++ b/internal/server/static/serviceworker.js @@ -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})); + } +}); \ No newline at end of file diff --git a/main.go b/main.go new file mode 100644 index 0000000..75cab57 --- /dev/null +++ b/main.go @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright © 2022 Roberto Hidalgo +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) + } +} diff --git a/schema.sql b/schema.sql new file mode 100644 index 0000000..2210996 --- /dev/null +++ b/schema.sql @@ -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); \ No newline at end of file