diff --git a/.golangci.yml b/.golangci.yml
new file mode 100644
index 0000000..8e3aa16
--- /dev/null
+++ b/.golangci.yml
@@ -0,0 +1,63 @@
+---
+run:
+ skip-files:
+ - boardvoting/assets.go
+
+output:
+ sort-results: true
+
+linter-settings:
+ goheader:
+ values:
+ const:
+ ORGANIZATION: CAcert Inc.
+ template: |
+ Copyright {{ YEAR-RANGE }} {{ ORGANIZATION }}
+ SPDX-License-Identifier: Apache-2.0
+
+ 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.
+ goimports:
+ local-prefixes: git.cacert.org/cacert-boardvoting
+ misspell:
+ locale: US
+ ignore-words:
+ - CAcert
+
+linters:
+ disable-all: false
+ enable:
+ - bodyclose
+ - errorlint
+ - gocognit
+ - goconst
+ - gocritic
+ - gofmt
+ - goheader
+ - goimports
+ - golint
+ - gomnd
+ - gosec
+ - interfacer
+ - lll
+ - makezero
+ - misspell
+ - nakedret
+ - nestif
+ - nlreturn
+ - nolintlint
+ - predeclared
+ - rowserrcheck
+ - scopelint
+ - sqlclosecheck
+ - wrapcheck
+ - wsl
diff --git a/boardvoting.go b/boardvoting.go
index dca1985..f1d9933 100644
--- a/boardvoting.go
+++ b/boardvoting.go
@@ -1,18 +1,20 @@
/*
- Copyright 2017-2020 Jan Dittberner
+Copyright 2017-2021 Jan Dittberner
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this program except in compliance with the License.
- You may obtain a copy of the License at
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this program except in compliance with the License.
+You may obtain a copy of the License at
- http://www.apache.org/licenses/LICENSE-2.0
+ 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.
+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.
*/
+
+// The CAcert board voting software.
package main
import (
@@ -51,12 +53,21 @@ var csrfKey []byte
var version = "undefined"
var build = "undefined"
-const sessionCookieName = "votesession"
+const (
+ cookieSecretMinLen = 32
+ csrfKeyLength = 32
+ httpIdleTimeout = 5
+ httpReadHeaderTimeout = 10
+ httpReadTimeout = 10
+ httpWriteTimeout = 60
+ sessionCookieName = "votesession"
+)
func renderTemplate(w http.ResponseWriter, r *http.Request, templates []string, context interface{}) {
funcMaps := sprig.FuncMap()
funcMaps["nl2br"] = func(text string) template.HTML {
- return template.HTML(strings.Replace(template.HTMLEscapeString(text), "\n", "
", -1))
+ // #nosec G203 input is sanitized
+ return template.HTML(strings.ReplaceAll(template.HTMLEscapeString(text), "\n", "
"))
}
funcMaps[csrf.TemplateTag] = func() template.HTML {
return csrf.TemplateField(r)
@@ -65,20 +76,24 @@ func renderTemplate(w http.ResponseWriter, r *http.Request, templates []string,
var baseTemplate *template.Template
for count, t := range templates {
- var err error
- var assetBytes []byte
- if //noinspection GoUnresolvedReference
- assetBytes, err = boardvoting.Asset(fmt.Sprintf("templates/%s", t)); err != nil {
+ var (
+ err error
+ assetBytes []byte
+ )
+
+ if assetBytes, err = boardvoting.Asset(fmt.Sprintf("templates/%s", t)); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
+
+ return
+ }
+
+ if count == 0 {
+ if baseTemplate, err = template.New(t).Funcs(funcMaps).Parse(string(assetBytes)); err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ }
} else {
- if count == 0 {
- if baseTemplate, err = template.New(t).Funcs(funcMaps).Parse(string(assetBytes)); err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
- }
- } else {
- if _, err := baseTemplate.New(t).Funcs(funcMaps).Parse(string(assetBytes)); err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
- }
+ if _, err := baseTemplate.New(t).Funcs(funcMaps).Parse(string(assetBytes)); err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
}
@@ -100,27 +115,35 @@ const (
func authenticateRequest(w http.ResponseWriter, r *http.Request, handler func(http.ResponseWriter, *http.Request)) {
emailsTried := make(map[string]bool)
+
for _, cert := range r.TLS.PeerCertificates {
for _, extKeyUsage := range cert.ExtKeyUsage {
- if extKeyUsage == x509.ExtKeyUsageClientAuth {
- for _, emailAddress := range cert.EmailAddresses {
- emailLower := strings.ToLower(emailAddress)
- emailsTried[emailLower] = true
- voter, err := FindVoterByAddress(emailLower)
- if err != nil {
- http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
- return
- }
- if voter != nil {
- requestContext := context.WithValue(r.Context(), ctxVoter, voter)
- requestContext = context.WithValue(requestContext, ctxAuthenticatedCert, cert)
- handler(w, r.WithContext(requestContext))
- return
- }
+ if extKeyUsage != x509.ExtKeyUsageClientAuth {
+ continue
+ }
+
+ for _, emailAddress := range cert.EmailAddresses {
+ emailLower := strings.ToLower(emailAddress)
+ emailsTried[emailLower] = true
+
+ voter, err := FindVoterByAddress(emailLower)
+ if err != nil {
+ http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
+
+ return
+ }
+
+ if voter != nil {
+ requestContext := context.WithValue(r.Context(), ctxVoter, voter)
+ requestContext = context.WithValue(requestContext, ctxAuthenticatedCert, cert)
+ handler(w, r.WithContext(requestContext))
+
+ return
}
}
}
}
+
needsAuth, ok := r.Context().Value(ctxNeedsAuth).(bool)
if ok && needsAuth {
var templateContext struct {
@@ -129,14 +152,18 @@ func authenticateRequest(w http.ResponseWriter, r *http.Request, handler func(ht
Flashes interface{}
Emails []string
}
+
for k := range emailsTried {
templateContext.Emails = append(templateContext.Emails, k)
}
+
sort.Strings(templateContext.Emails)
w.WriteHeader(http.StatusForbidden)
renderTemplate(w, r, []string{"denied.html", "header.html", "footer.html"}, templateContext)
+
return
}
+
handler(w, r)
}
@@ -154,6 +181,7 @@ type motionListParameters struct {
func parseMotionParameters(r *http.Request) motionParameters {
var m = motionParameters{}
m.ShowVotes, _ = strconv.ParseBool(r.URL.Query().Get("showvotes"))
+
return m
}
@@ -164,20 +192,24 @@ func parseMotionListParameters(r *http.Request) motionListParameters {
} else {
m.Page = page
}
+
m.Flags.Withdraw, _ = strconv.ParseBool(r.URL.Query().Get("withdraw"))
m.Flags.Unvoted, _ = strconv.ParseBool(r.URL.Query().Get("unvoted"))
if r.Method == http.MethodPost {
m.Flags.Confirmed, _ = strconv.ParseBool(r.PostFormValue("confirm"))
}
+
return m
}
func motionListHandler(w http.ResponseWriter, r *http.Request) {
params := parseMotionListParameters(r)
+
session, err := store.Get(r, sessionCookieName)
if err != nil {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
+
return
}
@@ -189,30 +221,42 @@ func motionListHandler(w http.ResponseWriter, r *http.Request) {
PageTitle string
Flashes interface{}
}
+
if voter, ok := r.Context().Value(ctxVoter).(*Voter); ok {
templateContext.Voter = voter
}
+
if flashes := session.Flashes(); len(flashes) > 0 {
templateContext.Flashes = flashes
}
+
err = session.Save(r, w)
if err != nil {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
+
return
}
+
templateContext.Params = ¶ms
- if templateContext.Decisions, err = FindDecisionsForDisplayOnPage(params.Page, params.Flags.Unvoted, templateContext.Voter); err != nil {
+ if templateContext.Decisions, err = FindDecisionsForDisplayOnPage(
+ params.Page, params.Flags.Unvoted, templateContext.Voter,
+ ); err != nil {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
+
return
}
if len(templateContext.Decisions) > 0 {
- olderExists, err := templateContext.Decisions[len(templateContext.Decisions)-1].OlderExists(params.Flags.Unvoted, templateContext.Voter)
+ olderExists, err := templateContext.Decisions[len(templateContext.Decisions)-1].OlderExists(
+ params.Flags.Unvoted, templateContext.Voter,
+ )
if err != nil {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
+
return
}
+
if olderExists {
templateContext.NextPage = params.Page + 1
}
@@ -233,6 +277,7 @@ func motionHandler(w http.ResponseWriter, r *http.Request) {
decision, ok := getDecisionFromRequest(r)
if !ok {
http.Error(w, http.StatusText(http.StatusPreconditionFailed), http.StatusPreconditionFailed)
+
return
}
@@ -244,32 +289,53 @@ func motionHandler(w http.ResponseWriter, r *http.Request) {
PageTitle string
Flashes interface{}
}
+
voter, ok := getVoterFromRequest(r)
if ok {
templateContext.Voter = voter
}
+
templateContext.Params = ¶ms
+
if params.ShowVotes {
if err := decision.LoadVotes(); err != nil {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
+
return
}
}
+
templateContext.Decision = decision
templateContext.PageTitle = fmt.Sprintf("Motion %s: %s", decision.Tag, decision.Title)
- renderTemplate(w, r, []string{"motion.html", "motion_fragments.html", "page_fragments.html", "header.html", "footer.html"}, templateContext)
+
+ renderTemplate(w, r, []string{
+ "motion.html",
+ "motion_fragments.html",
+ "page_fragments.html",
+ "header.html",
+ "footer.html",
+ }, templateContext)
}
-func singleDecisionHandler(w http.ResponseWriter, r *http.Request, tag string, handler func(http.ResponseWriter, *http.Request)) {
+func singleDecisionHandler(
+ w http.ResponseWriter,
+ r *http.Request,
+ tag string,
+ handler func(http.ResponseWriter, *http.Request),
+) {
decision, err := FindDecisionForDisplayByTag(tag)
if err != nil {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
+
return
}
+
if decision == nil {
http.NotFound(w, r)
+
return
}
+
handler(w, r.WithContext(context.WithValue(r.Context(), ctxDecision, decision)))
}
@@ -286,16 +352,19 @@ func (authenticationRequiredHandler) NeedsAuth() bool {
func getVoterFromRequest(r *http.Request) (voter *Voter, ok bool) {
voter, ok = r.Context().Value(ctxVoter).(*Voter)
+
return
}
func getDecisionFromRequest(r *http.Request) (decision *DecisionForDisplay, ok bool) {
decision, ok = r.Context().Value(ctxDecision).(*DecisionForDisplay)
+
return
}
func getVoteFromRequest(r *http.Request) (vote VoteChoice, ok bool) {
vote, ok = r.Context().Value(ctxVote).(VoteChoice)
+
return
}
@@ -305,12 +374,16 @@ func (a *FlashMessageAction) AddFlash(w http.ResponseWriter, r *http.Request, me
session, err := store.Get(r, sessionCookieName)
if err != nil {
log.Warnf("could not get session cookie: %v", err)
+
return
}
+
session.AddFlash(message, tags...)
+
err = session.Save(r, w)
if err != nil {
log.Warnf("could not save flash message: %v", err)
+
return
}
}
@@ -324,14 +397,25 @@ func (a *withDrawMotionAction) Handle(w http.ResponseWriter, r *http.Request) {
decision, ok := getDecisionFromRequest(r)
if !ok || decision.Status != voteStatusPending {
http.Error(w, http.StatusText(http.StatusPreconditionFailed), http.StatusPreconditionFailed)
+
return
}
+
voter, ok := getVoterFromRequest(r)
if !ok {
http.Error(w, http.StatusText(http.StatusPreconditionFailed), http.StatusPreconditionFailed)
+
return
}
- templates := []string{"withdraw_motion_form.html", "header.html", "footer.html", "motion_fragments.html", "page_fragments.html"}
+
+ templates := []string{
+ "withdraw_motion_form.html",
+ "header.html",
+ "footer.html",
+ "motion_fragments.html",
+ "page_fragments.html",
+ }
+
var templateContext struct {
PageTitle string
Decision *DecisionForDisplay
@@ -343,9 +427,11 @@ func (a *withDrawMotionAction) Handle(w http.ResponseWriter, r *http.Request) {
case http.MethodPost:
decision.Status = voteStatusWithdrawn
decision.Modified = time.Now().UTC()
+
if err := decision.UpdateStatus(); err != nil {
log.Errorf("withdrawing motion failed: %v", err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
+
return
}
@@ -372,12 +458,14 @@ func (h *newMotionHandler) Handle(w http.ResponseWriter, r *http.Request) {
}
templates := []string{"create_motion_form.html", "page_fragments.html", "header.html", "footer.html"}
+
var templateContext struct {
Form NewDecisionForm
PageTitle string
Voter *Voter
Flashes interface{}
}
+
switch r.Method {
case http.MethodPost:
form := NewDecisionForm{
@@ -393,10 +481,11 @@ func (h *newMotionHandler) Handle(w http.ResponseWriter, r *http.Request) {
renderTemplate(w, r, templates, templateContext)
} else {
data.Proposed = time.Now().UTC()
- data.ProponentId = voter.Id
+ data.ProponentID = voter.ID
if err := data.Create(); err != nil {
log.Errorf("saving motion failed: %v", err)
http.Error(w, "Saving motion failed", http.StatusInternalServerError)
+
return
}
@@ -426,20 +515,26 @@ func (a editMotionAction) Handle(w http.ResponseWriter, r *http.Request) {
decision, ok := getDecisionFromRequest(r)
if !ok || decision.Status != voteStatusPending {
http.Error(w, http.StatusText(http.StatusPreconditionFailed), http.StatusPreconditionFailed)
+
return
}
+
voter, ok := getVoterFromRequest(r)
if !ok {
http.Error(w, http.StatusText(http.StatusPreconditionFailed), http.StatusPreconditionFailed)
+
return
}
+
templates := []string{"edit_motion_form.html", "page_fragments.html", "header.html", "footer.html"}
+
var templateContext struct {
Form EditDecisionForm
PageTitle string
Voter *Voter
Flashes interface{}
}
+
switch r.Method {
case http.MethodPost:
form := EditDecisionForm{
@@ -459,6 +554,7 @@ func (a editMotionAction) Handle(w http.ResponseWriter, r *http.Request) {
if err := data.Update(); err != nil {
log.Errorf("updating motion failed: %v", err)
http.Error(w, "Updating the motion failed.", http.StatusInternalServerError)
+
return
}
@@ -468,6 +564,7 @@ func (a editMotionAction) Handle(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "/motions/", http.StatusMovedPermanently)
}
+
return
default:
templateContext.Voter = voter
@@ -494,35 +591,43 @@ func (h motionsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
switch {
case subURL == "":
authenticateRequest(w, r.WithContext(context.WithValue(r.Context(), ctxNeedsAuth, false)), motionListHandler)
+
return
case subURL == "/newmotion/":
handler := &newMotionHandler{}
authenticateRequest(
w, r.WithContext(context.WithValue(r.Context(), ctxNeedsAuth, true)),
handler.Handle)
+
return
case strings.Count(subURL, "/") == 1:
parts := strings.Split(subURL, "/")
motionTag := parts[0]
+
action, ok := motionActionMap[parts[1]]
if !ok {
http.NotFound(w, r)
+
return
}
+
authenticateRequest(
w, r.WithContext(context.WithValue(r.Context(), ctxNeedsAuth, action.NeedsAuth())),
func(w http.ResponseWriter, r *http.Request) {
singleDecisionHandler(w, r, motionTag, action.Handle)
})
+
return
case strings.Count(subURL, "/") == 0:
authenticateRequest(w, r.WithContext(context.WithValue(r.Context(), ctxNeedsAuth, false)),
func(w http.ResponseWriter, r *http.Request) {
singleDecisionHandler(w, r, subURL, motionHandler)
})
+
return
default:
http.NotFound(w, r)
+
return
}
}
@@ -536,26 +641,33 @@ func (h *directVoteHandler) Handle(w http.ResponseWriter, r *http.Request) {
decision, ok := getDecisionFromRequest(r)
if !ok {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
+
return
}
+
voter, ok := getVoterFromRequest(r)
if !ok {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
+
return
}
+
vote, ok := getVoteFromRequest(r)
if !ok {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
+
return
}
+
switch r.Method {
case http.MethodPost:
voteResult := &Vote{
- VoterId: voter.Id, Vote: vote, DecisionId: decision.Id, Voted: time.Now().UTC(),
+ VoterID: voter.ID, Vote: vote, DecisionID: decision.ID, Voted: time.Now().UTC(),
Notes: fmt.Sprintf("Direct Vote\n\n%s", getPEMClientCert(r))}
if err := voteResult.Save(); err != nil {
log.Errorf("Problem saving vote: %v", err)
http.Error(w, "Problem saving vote", http.StatusInternalServerError)
+
return
}
@@ -565,7 +677,14 @@ func (h *directVoteHandler) Handle(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "/motions/", http.StatusMovedPermanently)
default:
- templates := []string{"direct_vote_form.html", "header.html", "footer.html", "motion_fragments.html", "page_fragments.html"}
+ templates := []string{
+ "direct_vote_form.html",
+ "header.html",
+ "footer.html",
+ "motion_fragments.html",
+ "page_fragments.html",
+ }
+
var templateContext struct {
Decision *DecisionForDisplay
VoteChoice VoteChoice
@@ -573,9 +692,11 @@ func (h *directVoteHandler) Handle(w http.ResponseWriter, r *http.Request) {
Flashes interface{}
Voter *Voter
}
+
templateContext.Decision = decision
templateContext.VoteChoice = vote
templateContext.Voter = voter
+
renderTemplate(w, r, templates, templateContext)
}
}
@@ -588,10 +709,12 @@ type proxyVoteHandler struct {
func getPEMClientCert(r *http.Request) string {
clientCertPEM := bytes.NewBufferString("")
authenticatedCertificate := r.Context().Value(ctxAuthenticatedCert).(*x509.Certificate)
+
err := pem.Encode(clientCertPEM, &pem.Block{Type: "CERTIFICATE", Bytes: authenticatedCertificate.Raw})
if err != nil {
log.Errorf("error encoding client certificate: %v", err)
}
+
return clientCertPEM.String()
}
@@ -599,14 +722,25 @@ func (h *proxyVoteHandler) Handle(w http.ResponseWriter, r *http.Request) {
decision, ok := getDecisionFromRequest(r)
if !ok {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
+
return
}
+
proxy, ok := getVoterFromRequest(r)
if !ok {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
+
return
}
- templates := []string{"proxy_vote_form.html", "header.html", "footer.html", "motion_fragments.html", "page_fragments.html"}
+
+ templates := []string{
+ "proxy_vote_form.html",
+ "header.html",
+ "footer.html",
+ "motion_fragments.html",
+ "page_fragments.html",
+ }
+
var templateContext struct {
Form ProxyVoteForm
Decision *DecisionForDisplay
@@ -615,7 +749,9 @@ func (h *proxyVoteHandler) Handle(w http.ResponseWriter, r *http.Request) {
Flashes interface{}
Voter *Voter
}
+
templateContext.Voter = proxy
+
switch r.Method {
case http.MethodPost:
form := ProxyVoteForm{
@@ -627,15 +763,19 @@ func (h *proxyVoteHandler) Handle(w http.ResponseWriter, r *http.Request) {
if valid, voter, data, justification := form.Validate(); !valid {
templateContext.Form = form
templateContext.Decision = decision
- if voters, err := GetVotersForProxy(proxy); err != nil {
+
+ voters, err := GetVotersForProxy(proxy)
+ if err != nil {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
+
return
- } else {
- templateContext.Voters = voters
}
+
+ templateContext.Voters = voters
+
renderTemplate(w, r, templates, templateContext)
} else {
- data.DecisionId = decision.Id
+ data.DecisionID = decision.ID
data.Voted = time.Now().UTC()
data.Notes = fmt.Sprintf(
"Proxy-Vote by %s\n\n%s\n\n%s",
@@ -644,6 +784,7 @@ func (h *proxyVoteHandler) Handle(w http.ResponseWriter, r *http.Request) {
if err := data.Save(); err != nil {
log.Errorf("Error saving vote: %s", err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
+
return
}
@@ -653,16 +794,21 @@ func (h *proxyVoteHandler) Handle(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "/motions/", http.StatusMovedPermanently)
}
+
return
default:
templateContext.Form = ProxyVoteForm{}
templateContext.Decision = decision
- if voters, err := GetVotersForProxy(proxy); err != nil {
+
+ voters, err := GetVotersForProxy(proxy)
+ if err != nil {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
+
return
- } else {
- templateContext.Voters = voters
}
+
+ templateContext.Voters = voters
+
renderTemplate(w, r, templates, templateContext)
}
}
@@ -674,24 +820,33 @@ func (h *decisionVoteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
case strings.HasPrefix(r.URL.Path, "/proxy/"):
motionTag := r.URL.Path[len("/proxy/"):]
handler := &proxyVoteHandler{}
+
authenticateRequest(
w, r.WithContext(context.WithValue(r.Context(), ctxNeedsAuth, true)),
func(w http.ResponseWriter, r *http.Request) {
singleDecisionHandler(w, r, motionTag, handler.Handle)
})
case strings.HasPrefix(r.URL.Path, "/vote/"):
+ const expectedParts = 2
+
parts := strings.Split(r.URL.Path[len("/vote/"):], "/")
- if len(parts) != 2 {
+ if len(parts) != expectedParts {
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
+
return
}
+
motionTag := parts[0]
+
voteValue, ok := VoteValues[parts[1]]
if !ok {
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
+
return
}
+
handler := &directVoteHandler{}
+
authenticateRequest(
w, r.WithContext(context.WithValue(r.Context(), ctxNeedsAuth, true)),
func(w http.ResponseWriter, r *http.Request) {
@@ -699,6 +854,7 @@ func (h *decisionVoteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
w, r.WithContext(context.WithValue(r.Context(), ctxVote, voteValue)),
motionTag, handler.Handle)
})
+
return
}
}
@@ -714,8 +870,8 @@ type Config struct {
CookieSecret string `yaml:"cookie_secret"`
CsrfKey string `yaml:"csrf_key"`
BaseURL string `yaml:"base_url"`
- HttpAddress string `yaml:"http_address"`
- HttpsAddress string `yaml:"https_address"`
+ HTTPAddress string `yaml:"http_address"`
+ HTTPSAddress string `yaml:"https_address"`
MailServer struct {
Host string `yaml:"host"`
Port int `yaml:"port"`
@@ -727,15 +883,17 @@ func readConfig() {
if err != nil {
log.Panicf("Opening configuration file failed: %v", err)
}
+
if err := yaml.Unmarshal(source, &config); err != nil {
log.Panicf("Loading configuration failed: %v", err)
}
- if config.HttpsAddress == "" {
- config.HttpsAddress = "127.0.0.1:8443"
+ if config.HTTPSAddress == "" {
+ config.HTTPSAddress = "127.0.0.1:8443"
}
- if config.HttpAddress == "" {
- config.HttpAddress = "127.0.0.1:8080"
+
+ if config.HTTPAddress == "" {
+ config.HTTPAddress = "127.0.0.1:8080"
}
cookieSecret, err := base64.StdEncoding.DecodeString(config.CookieSecret)
@@ -743,17 +901,26 @@ func readConfig() {
log.Panicf("Decoding cookie secret failed: %v", err)
panic(err)
}
- if len(cookieSecret) < 32 {
- log.Panic("Cookie secret is less than 32 bytes long")
+
+ if len(cookieSecret) < cookieSecretMinLen {
+ log.Panicf("Cookie secret is less than %d bytes long", cookieSecretMinLen)
}
+
csrfKey, err = base64.StdEncoding.DecodeString(config.CsrfKey)
if err != nil {
log.Panicf("Decoding csrf key failed: %v", err)
}
- if len(csrfKey) != 32 {
- log.Panicf("CSRF key must be exactly 32 bytes long but is %d bytes long", len(csrfKey))
+
+ if len(csrfKey) != csrfKeyLength {
+ log.Panicf(
+ "CSRF key must be exactly %d bytes long but is %d bytes long",
+ csrfKeyLength,
+ len(csrfKey),
+ )
}
+
store = sessions.NewCookieStore(cookieSecret)
+
log.Info("Read configuration")
}
@@ -762,6 +929,7 @@ func setupDbConfig(ctx context.Context) {
if err != nil {
log.Panicf("Opening database failed: %v", err)
}
+
db = NewDB(database)
go func() {
@@ -812,6 +980,7 @@ func setupTLSConfig() (tlsConfig *tls.Config) {
if err != nil {
log.Panicf("Error reading client certificate CAs %v", err)
}
+
caCertPool := x509.NewCertPool()
if !caCertPool.AppendCertsFromPEM(caCert) {
log.Panic("could not initialize client CA certificate pool")
@@ -819,9 +988,11 @@ func setupTLSConfig() (tlsConfig *tls.Config) {
// setup HTTPS server
tlsConfig = &tls.Config{
+ MinVersion: tls.VersionTLS12,
ClientCAs: caCertPool,
ClientAuth: tls.VerifyClientCertIfGiven,
}
+
return
}
@@ -835,23 +1006,26 @@ func main() {
flag.Parse()
var stopAll func()
+
executionContext, stopAll := context.WithCancel(context.Background())
+
readConfig()
setupDbConfig(executionContext)
setupNotifications(executionContext)
setupJobs(executionContext)
setupHandlers()
+
tlsConfig := setupTLSConfig()
defer stopAll()
server := &http.Server{
- Addr: config.HttpsAddress,
+ Addr: config.HTTPSAddress,
TLSConfig: tlsConfig,
- IdleTimeout: time.Second * 120,
- ReadHeaderTimeout: time.Second * 10,
- ReadTimeout: time.Second * 20,
- WriteTimeout: time.Second * 60,
+ IdleTimeout: time.Second * httpIdleTimeout,
+ ReadHeaderTimeout: time.Second * httpReadHeaderTimeout,
+ ReadTimeout: time.Second * httpReadTimeout,
+ WriteTimeout: time.Second * httpWriteTimeout,
}
server.Handler = csrf.Protect(csrfKey)(http.DefaultServeMux)
@@ -859,24 +1033,27 @@ func main() {
log.Infof("Launching application on https://%s/", server.Addr)
errs := make(chan error, 1)
+
go func() {
httpRedirector := &http.Server{
- Addr: config.HttpAddress,
+ Addr: config.HTTPAddress,
Handler: http.RedirectHandler(config.BaseURL, http.StatusMovedPermanently),
- IdleTimeout: time.Second * 5,
- ReadHeaderTimeout: time.Second * 10,
- ReadTimeout: time.Second * 10,
- WriteTimeout: time.Second * 60,
+ IdleTimeout: time.Second * httpIdleTimeout,
+ ReadHeaderTimeout: time.Second * httpReadHeaderTimeout,
+ ReadTimeout: time.Second * httpReadTimeout,
+ WriteTimeout: time.Second * httpWriteTimeout,
}
if err := httpRedirector.ListenAndServe(); err != nil {
errs <- err
}
+
close(errs)
}()
if err := server.ListenAndServeTLS(config.ServerCert, config.ServerKey); err != nil {
log.Panicf("ListenAndServerTLS failed: %v", err)
}
+
if err := <-errs; err != nil {
log.Panicf("ListenAndServe failed: %v", err)
}
diff --git a/boardvoting/main.go b/boardvoting/main.go
index 718276e..0997faa 100644
--- a/boardvoting/main.go
+++ b/boardvoting/main.go
@@ -1,18 +1,20 @@
/*
- Copyright 2017-2019 Jan Dittberner
+Copyright 2017-2021 Jan Dittberner
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this program except in compliance with the License.
- You may obtain a copy of the License at
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this program except in compliance with the License.
+You may obtain a copy of the License at
- http://www.apache.org/licenses/LICENSE-2.0
+ 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.
+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.
*/
+
+// Assets for the CAcert board voting software.
package boardvoting
//go:generate go-bindata -pkg $GOPACKAGE -o assets.go ./migrations/... ./static/... ./templates/...
@@ -29,8 +31,11 @@ import (
"time"
)
+const defaultFileMode = 0400
+
var defaultFileTimestamp = time.Now()
+// Simulated asset file system.
type AssetFS struct {
Asset func(path string) ([]byte, error)
AssetDir func(path string) ([]string, error)
@@ -46,16 +51,19 @@ type FakeFile struct {
func (f *FakeFile) Name() string {
_, name := filepath.Split(f.Path)
+
return name
}
func (f *FakeFile) Size() int64 { return f.Len }
func (f *FakeFile) Mode() os.FileMode {
- mode := os.FileMode(0644)
+ mode := os.FileMode(defaultFileMode)
+
if f.Dir {
return mode | os.ModeDir
}
+
return mode
}
@@ -73,6 +81,7 @@ func NewAssetFile(name string, content []byte, timestamp time.Time) *AssetFile {
if timestamp.IsZero() {
timestamp = defaultFileTimestamp
}
+
return &AssetFile{
bytes.NewReader(content),
ioutil.NopCloser(nil),
@@ -80,7 +89,7 @@ func NewAssetFile(name string, content []byte, timestamp time.Time) *AssetFile {
}
}
-func (f *AssetFile) Readdir(count int) ([]os.FileInfo, error) {
+func (f *AssetFile) Readdir(int) ([]os.FileInfo, error) {
return nil, errors.New("not a directory")
}
@@ -102,24 +111,31 @@ func (f *AssetDirectory) Readdir(count int) ([]os.FileInfo, error) {
if count <= 0 {
return f.Children, nil
}
+
if f.ChildrenRead+count > len(f.Children) {
count = len(f.Children) - f.ChildrenRead
}
+
rv := f.Children[f.ChildrenRead : f.ChildrenRead+count]
f.ChildrenRead += count
+
return rv, nil
}
+// Simulate os.Stat for a simulated asset directory.
func (f *AssetDirectory) Stat() (os.FileInfo, error) {
return f, nil
}
+// Return a new asset directory.
func NewAssetDirectory(name string, children []string, fs *AssetFS) *AssetDirectory {
fileInfos := make([]os.FileInfo, 0, len(children))
+
for _, child := range children {
_, err := fs.AssetDir(filepath.Join(name, child))
fileInfos = append(fileInfos, &FakeFile{child, err == nil, 0, time.Time{}})
}
+
return &AssetDirectory{
AssetFile{
bytes.NewReader(nil),
@@ -130,29 +146,37 @@ func NewAssetDirectory(name string, children []string, fs *AssetFS) *AssetDirect
fileInfos}
}
+// Open named simulated file.
func (f *AssetFS) Open(name string) (http.File, error) {
if len(name) > 0 && name[0] == '/' {
name = name[1:]
}
+
if b, err := f.Asset(name); err == nil {
timestamp := defaultFileTimestamp
+
if f.AssetInfo != nil {
if info, err := f.AssetInfo(name); err == nil {
timestamp = info.ModTime()
}
}
+
return NewAssetFile(name, b, timestamp), nil
}
- if children, err := f.AssetDir(name); err == nil {
+
+ children, err := f.AssetDir(name)
+ if err == nil {
return NewAssetDirectory(name, children, f), nil
- } else {
- if strings.Contains(err.Error(), "not found") {
- return nil, os.ErrNotExist
- }
- return nil, err
}
+
+ if strings.Contains(err.Error(), "not found") {
+ return nil, os.ErrNotExist
+ }
+
+ return nil, err
}
+// Get the simulated asset filesystem initialized with generated values.
func GetAssetFS() *AssetFS {
return &AssetFS{Asset: Asset, AssetDir: AssetDir, AssetInfo: AssetInfo}
}
diff --git a/db/migrations_embedded.go b/db/migrations_embedded.go
index 8aa355a..d95b6d9 100644
--- a/db/migrations_embedded.go
+++ b/db/migrations_embedded.go
@@ -1,23 +1,24 @@
/*
- Copyright 2017-2019 Jan Dittberner
+Copyright 2017-2020 Jan Dittberner
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this program except in compliance with the License.
- You may obtain a copy of the License at
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this program except in compliance with the License.
+You may obtain a copy of the License at
- http://www.apache.org/licenses/LICENSE-2.0
+ 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.
+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.
*/
package db
import (
+ migrate "github.com/rubenv/sql-migrate"
+
"git.cacert.org/cacert-boardvoting/boardvoting"
- "github.com/rubenv/sql-migrate"
)
func Migrations() migrate.MigrationSource {
diff --git a/forms.go b/forms.go
index 9af6cf1..8d573bc 100644
--- a/forms.go
+++ b/forms.go
@@ -1,18 +1,19 @@
/*
- Copyright 2017-2019 Jan Dittberner
+Copyright 2017-2021 Jan Dittberner
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this program except in compliance with the License.
- You may obtain a copy of the License at
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this program except in compliance with the License.
+You may obtain a copy of the License at
- http://www.apache.org/licenses/LICENSE-2.0
+ 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.
+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.
*/
+
package main
import (
@@ -22,11 +23,24 @@ import (
"time"
)
+const (
+ minimumContentLen = 3
+ minimumTitleLen = 3
+)
+
+const (
+ hoursInADay = 24
+ dueThreeDays = 3
+ dueOneWeek = 7
+ dueTwoWeeks = 14
+ dueFourWeeks = 28
+)
+
var validDueDurations = map[string]time.Duration{
- "+3 days": time.Hour * 24 * 3,
- "+7 days": time.Hour * 24 * 7,
- "+14 days": time.Hour * 24 * 14,
- "+28 days": time.Hour * 24 * 28,
+ "+3 days": time.Hour * hoursInADay * dueThreeDays,
+ "+7 days": time.Hour * hoursInADay * dueOneWeek,
+ "+14 days": time.Hour * hoursInADay * dueTwoWeeks,
+ "+28 days": time.Hour * hoursInADay * dueFourWeeks,
}
type NewDecisionForm struct {
@@ -43,13 +57,13 @@ func (f *NewDecisionForm) Validate() (bool, *Decision) {
data := &Decision{}
data.Title = strings.TrimSpace(f.Title)
- if len(data.Title) < 3 {
- f.Errors["Title"] = "Please enter at least 3 characters for Title."
+ if len(data.Title) < minimumTitleLen {
+ f.Errors["Title"] = fmt.Sprintf("Please enter at least %d characters for Title.", minimumTitleLen)
}
data.Content = strings.TrimSpace(f.Content)
- if len(strings.Fields(data.Content)) < 3 {
- f.Errors["Content"] = "Please enter at least 3 words as Text."
+ if len(strings.Fields(data.Content)) < minimumContentLen {
+ f.Errors["Content"] = fmt.Sprintf("Please enter at least %d words as Text.", minimumContentLen)
}
if voteType, err := strconv.ParseUint(f.VoteType, 10, 8); err != nil || (voteType != 0 && voteType != 1) {
@@ -83,13 +97,13 @@ func (f *EditDecisionForm) Validate() (bool, *Decision) {
data := f.Decision
data.Title = strings.TrimSpace(f.Title)
- if len(data.Title) < 3 {
- f.Errors["Title"] = "Please enter at least 3 characters for Title."
+ if len(data.Title) < minimumTitleLen {
+ f.Errors["Title"] = fmt.Sprintf("Please enter at least %d characters for Title.", minimumTitleLen)
}
data.Content = strings.TrimSpace(f.Content)
- if len(strings.Fields(data.Content)) < 3 {
- f.Errors["Content"] = "Please enter at least 3 words as Text."
+ if len(strings.Fields(data.Content)) < minimumContentLen {
+ f.Errors["Content"] = fmt.Sprintf("Please enter at least %d words as Text.", minimumContentLen)
}
if voteType, err := strconv.ParseUint(f.VoteType, 10, 8); err != nil || (voteType != 0 && voteType != 1) {
@@ -118,27 +132,34 @@ type ProxyVoteForm struct {
func (f *ProxyVoteForm) Validate() (bool, *Voter, *Vote, string) {
f.Errors = make(map[string]string)
+ const minimumJustificationLen = 3
+
+ var (
+ voter *Voter
+ err error
+ voterID, vote int64
+ )
+
data := &Vote{}
- var voter *Voter
- if voterId, err := strconv.ParseInt(f.Voter, 10, 64); err != nil {
- f.Errors["Voter"] = fmt.Sprint("Please choose a valid voter.", err)
- } else if voter, err = GetVoterById(voterId); err != nil {
- f.Errors["Voter"] = fmt.Sprint("Please choose a valid voter.", err)
+ if voterID, err = strconv.ParseInt(f.Voter, 10, 64); err != nil {
+ f.Errors["Voter"] = fmt.Sprintf("Please choose a valid voter: %v.", err)
+ } else if voter, err = GetVoterByID(voterID); err != nil {
+ f.Errors["Voter"] = fmt.Sprintf("Please choose a valid voter: %v.", err)
} else {
- data.VoterId = voter.Id
+ data.VoterID = voter.ID
}
- if vote, err := strconv.ParseInt(f.Vote, 10, 8); err != nil {
- f.Errors["Vote"] = fmt.Sprint("Please choose a valid vote.", err)
+ if vote, err = strconv.ParseInt(f.Vote, 10, 8); err != nil {
+ f.Errors["Vote"] = fmt.Sprintf("Please choose a valid vote: %v.", err)
} else if voteChoice, ok := VoteChoices[vote]; !ok {
- f.Errors["Vote"] = fmt.Sprint("Please choose a valid vote.")
+ f.Errors["Vote"] = "Please choose a valid vote."
} else {
data.Vote = voteChoice
}
justification := strings.TrimSpace(f.Justification)
- if len(justification) < 3 {
+ if len(justification) < minimumJustificationLen {
f.Errors["Justification"] = "Please enter at least 3 characters for justification."
}
diff --git a/go.mod b/go.mod
index 9aa52f8..02572a1 100644
--- a/go.mod
+++ b/go.mod
@@ -4,23 +4,26 @@ go 1.14
require (
github.com/Masterminds/goutils v1.1.0 // indirect
- github.com/Masterminds/semver v1.4.2 // indirect
- github.com/Masterminds/sprig v2.20.0+incompatible
+ github.com/Masterminds/semver v1.5.0 // indirect
+ github.com/Masterminds/sprig v2.22.0+incompatible
github.com/gobuffalo/packr v1.30.1 // indirect
- github.com/google/uuid v1.1.1 // indirect
- github.com/gorilla/csrf v1.6.0
- github.com/gorilla/sessions v1.2.0
- github.com/huandu/xstrings v1.2.0 // indirect
- github.com/imdario/mergo v0.3.7 // indirect
+ github.com/google/uuid v1.1.4 // indirect
+ github.com/gorilla/csrf v1.7.0
+ github.com/gorilla/sessions v1.2.1
+ github.com/huandu/xstrings v1.3.2 // indirect
+ github.com/imdario/mergo v0.3.11 // indirect
github.com/jmoiron/sqlx v1.2.0
- github.com/mattn/go-sqlite3 v1.11.0
- github.com/rubenv/sql-migrate v0.0.0-20190717103323-87ce952f7079
- github.com/sirupsen/logrus v1.4.2
+ github.com/konsorten/go-windows-terminal-sequences v1.0.3 // indirect
+ github.com/mattn/go-sqlite3 v1.14.6
+ github.com/mitchellh/copystructure v1.0.0 // indirect
+ github.com/mitchellh/reflectwalk v1.0.1 // indirect
+ github.com/rubenv/sql-migrate v0.0.0-20200616145509-8d140a17f351
+ github.com/sirupsen/logrus v1.7.0
github.com/ziutek/mymysql v1.5.4 // indirect
- golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4 // indirect
- google.golang.org/appengine v1.6.1 // indirect
+ golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad // indirect
+ golang.org/x/sys v0.0.0-20210108172913-0df2131ae363 // indirect
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df
gopkg.in/gorp.v1 v1.7.2 // indirect
- gopkg.in/yaml.v2 v2.2.2
+ gopkg.in/yaml.v2 v2.4.0
)
diff --git a/go.sum b/go.sum
index caf79c2..e900429 100644
--- a/go.sum
+++ b/go.sum
@@ -1,52 +1,191 @@
+cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
+cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
+github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0=
github.com/Masterminds/goutils v1.1.0 h1:zukEsf/1JZwCMgHiK3GZftabmxiCw4apj3a28RPBiVg=
github.com/Masterminds/goutils v1.1.0/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
github.com/Masterminds/semver v1.4.2 h1:WBLTQ37jOCzSLtXNdoo8bNM8876KhNqOKvrlGITgsTc=
github.com/Masterminds/semver v1.4.2/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y=
+github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww=
+github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y=
github.com/Masterminds/sprig v2.20.0+incompatible h1:dJTKKuUkYW3RMFdQFXPU/s6hg10RgctmTjRcbZ98Ap8=
github.com/Masterminds/sprig v2.20.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o=
+github.com/Masterminds/sprig v2.22.0+incompatible h1:z4yfnGrZ7netVz+0EDJ0Wi+5VZCSYp4Z0m2dk6cEM60=
+github.com/Masterminds/sprig v2.22.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o=
+github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo=
+github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI=
+github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g=
+github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c=
+github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
+github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
+github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
+github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
+github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
+github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
+github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
+github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
+github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
+github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A=
+github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU=
+github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
+github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g=
+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/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
+github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
+github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ=
+github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
+github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
+github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
+github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec/go.mod h1:jMjuTZXRI4dUb/I5gc9Hdhagfvm9+RyrPryS/auMzxE=
+github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
+github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8=
+github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI=
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-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
+github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/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-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
+github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
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.0.0-20191001013358-cfbb681360f0/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
+github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
+github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
+github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=
+github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU=
+github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
+github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M=
+github.com/envoyproxy/go-control-plane v0.6.9/go.mod h1:SBwIajubJHhxtWwsL9s8ss4safvEdbitLhGGK48rN6g=
+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.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
+github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVBjqR7JHJk0brhHOZYGmfBYOrK0ZhYMEtBr4=
+github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
+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/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
+github.com/go-kit/kit v0.10.0/go.mod h1:xUsJbQ/Fp4kEt7AFgCuvyX4a71u8h9jB8tj/ORgOZ7o=
+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-sql-driver/mysql v1.4.0 h1:7LxgVwFb2hIQtMm87NdgAVfXjnt4OePseqT1tKx+opk=
github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
+github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
+github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/gobuffalo/envy v1.7.0 h1:GlXgaiBkmrYMHco6t4j7SacKO4XUjvh5pwXh0f4uxXU=
github.com/gobuffalo/envy v1.7.0/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSCx4T5NI=
+github.com/gobuffalo/envy v1.7.1/go.mod h1:FurDp9+EDPE4aIUS3ZLyD+7/9fpx7YRt/ukY6jIHf0w=
github.com/gobuffalo/logger v1.0.0/go.mod h1:2zbswyIUa45I+c+FLXuWl9zSWEiVuthsk8ze5s8JvPs=
+github.com/gobuffalo/logger v1.0.1/go.mod h1:2zbswyIUa45I+c+FLXuWl9zSWEiVuthsk8ze5s8JvPs=
github.com/gobuffalo/packd v0.3.0 h1:eMwymTkA1uXsqxS0Tpoop3Lc0u3kTfiMBE6nKtQU4g4=
github.com/gobuffalo/packd v0.3.0/go.mod h1:zC7QkmNkYVGKPw4tHpBQ+ml7W/3tIebgeo1b36chA3Q=
github.com/gobuffalo/packr v1.30.1 h1:hu1fuVR3fXEZR7rXNW3h8rqSML8EVAf6KNm0NKO/wKg=
github.com/gobuffalo/packr v1.30.1/go.mod h1:ljMyFO2EcrnzsHsN99cvbq055Y9OhRrIaviy289eRuk=
github.com/gobuffalo/packr/v2 v2.5.1/go.mod h1:8f9c96ITobJlPzI44jj+4tHnEKNt0xXWSVlXRN9X1Iw=
+github.com/gobuffalo/packr/v2 v2.7.1/go.mod h1:qYEvAazPaVxy7Y7KR0W8qYEE+RymX74kETFqjFoFlOc=
+github.com/godror/godror v0.13.3/go.mod h1:2ouUT4kdhUBk7TAkHWD4SN0CdI0pgEQbo8FVHhbSKWg=
+github.com/gogo/googleapis v1.1.0/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s=
+github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
+github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
+github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
+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-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/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/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
+github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
+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/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
+github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/google/uuid v1.1.4 h1:0ecGp3skIrHWPNGPJDaBIghfA6Sp7Ruo2Io8eLKzWm0=
+github.com/google/uuid v1.1.4/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
+github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
github.com/gorilla/csrf v1.6.0 h1:60oN1cFdncCE8tjwQ3QEkFND5k37lQPcRjnlvm7CIJ0=
github.com/gorilla/csrf v1.6.0/go.mod h1:7tSf8kmjNYr7IWDCYhd3U8Ck34iQ/Yw5CJu7bAkHEGI=
+github.com/gorilla/csrf v1.7.0 h1:mMPjV5/3Zd460xCavIkppUdvnl5fPXMpv2uz2Zyg7/Y=
+github.com/gorilla/csrf v1.7.0/go.mod h1:+a/4tCmqhG6/w4oafeAZ9pEa3/NZOWYVbD9fV0FwIQA=
+github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
+github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
github.com/gorilla/sessions v1.2.0 h1:S7P+1Hm5V/AT9cjEcUD5uDaQSX0OE577aCXgoaKpYbQ=
github.com/gorilla/sessions v1.2.0/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
+github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI=
+github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
+github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
+github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/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.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
+github.com/hashicorp/consul/api v1.3.0/go.mod h1:MmDNSzIMUjNpY/mQ398R4bk2FnqQLoPndWW5VkKPlCE=
+github.com/hashicorp/consul/sdk v0.3.0/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
+github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
+github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
+github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
+github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
+github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
+github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU=
+github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
+github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
+github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
+github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
+github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
+github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=
+github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
+github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
+github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
+github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
+github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
+github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
+github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/huandu/xstrings v1.2.0 h1:yPeWdRnmynF7p+lLYz0H2tthW9lqhMJrQV/U7yy4wX0=
github.com/huandu/xstrings v1.2.0/go.mod h1:DvyZB1rfVYsBIigL8HwpZgxHwXozlTgGqn63UyNX5k4=
+github.com/huandu/xstrings v1.3.2 h1:L18LIDzqlW6xN2rEkpdV8+oL/IXWJ1APd+vsdYy4Wdw=
+github.com/huandu/xstrings v1.3.2/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
+github.com/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg=
github.com/imdario/mergo v0.3.7 h1:Y+UAYTZ7gDEuOfhxKWy+dvb5dRQ6rJjFSdX2HZY1/gI=
github.com/imdario/mergo v0.3.7/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
+github.com/imdario/mergo v0.3.11 h1:3tnifQM4i+fbajXKBHXWEH+KvNHqojZ778UH75j3bGA=
+github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
+github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo=
+github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
github.com/jmoiron/sqlx v1.2.0 h1:41Ip0zITnmWNR/vHV+S4m+VoUivnWY5E4OJfLZjCJMA=
github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks=
github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc=
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
+github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
+github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
+github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
+github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
+github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
+github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/karrick/godirwalk v1.10.12/go.mod h1:RoGL9dQei4vP9ilrpETWE8CLOZ1kiN0LhBygSwrAsHA=
+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 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s=
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
+github.com/konsorten/go-windows-terminal-sequences v1.0.3/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 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
@@ -54,75 +193,305 @@ github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/lib/pq v1.0.0 h1:X5PMW56eZitiTeO7tKzZxFCSpbFZJtkMMooicw2us9A=
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
+github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
+github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM=
+github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4=
+github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ=
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
+github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
+github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
+github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
+github.com/mattn/go-oci8 v0.0.7/go.mod h1:wjDx6Xm9q7dFtHJvIlrI99JytznLw5wQ4R+9mNXJwGI=
+github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
+github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/mattn/go-sqlite3 v1.11.0 h1:LDdKkqtYlom37fkvqs8rMPFKAMe8+SgjbwZ6ex1/A/Q=
github.com/mattn/go-sqlite3 v1.11.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
+github.com/mattn/go-sqlite3 v1.12.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
+github.com/mattn/go-sqlite3 v1.14.6 h1:dNPt6NO46WmLVt2DLNpwczCmdV5boIZ6g/tlDrlRUbg=
+github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
+github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
+github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
+github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
+github.com/mitchellh/copystructure v1.0.0 h1:Laisrj+bAB6b/yJwB5Bt3ITZhGJdqmxquMKeZ+mmkFQ=
+github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw=
+github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
+github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
+github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg=
+github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
+github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
+github.com/mitchellh/reflectwalk v1.0.0 h1:9D+8oIskB4VJBN5SFlmc27fSlIBZaov1Wpk/IfikLNY=
+github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
+github.com/mitchellh/reflectwalk v1.0.1 h1:FVzMWA5RllMAKIdUSC8mdWo3XtwoecrH79BY70sEEpE=
+github.com/mitchellh/reflectwalk v1.0.1/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
+github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
+github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
+github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
+github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg=
+github.com/nats-io/jwt v0.3.2/go.mod h1:/euKqTS1ZD+zzjYrY7pseZrTtWQSjujC7xjPc8wL6eU=
+github.com/nats-io/nats-server/v2 v2.1.2/go.mod h1:Afk+wRZqkMQs/p45uXdrVLuab3gwv3Z8C4HTBu8GD/k=
+github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzEE/Zbp4w=
+github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w=
+github.com/nats-io/nkeys v0.1.3/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w=
+github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
+github.com/oklog/oklog v0.3.2/go.mod h1:FCV+B7mhrz4o+ueLpx+KqkyXRGMWOYEvfiXtdGtbWGs=
+github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA=
+github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=
+github.com/olekukonko/tablewriter v0.0.1/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=
+github.com/olekukonko/tablewriter v0.0.2/go.mod h1:rSAaSIOAGT9odnlyGlUfAJaoc5w2fSBUmeGDbRWPxyQ=
+github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
+github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
+github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
+github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk=
+github.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492/go.mod h1:Ngi6UdF0k5OKD5t5wlmGhe/EDKPoUM3BXZSSfIuJbis=
+github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74=
+github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
+github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
+github.com/openzipkin-contrib/zipkin-go-opentracing v0.4.5/go.mod h1:/wsWhb9smxSfWAKL3wpBW7V8scJMt8N8gnaMCS9E/cA=
+github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw=
+github.com/openzipkin/zipkin-go v0.2.1/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4=
+github.com/openzipkin/zipkin-go v0.2.2/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4=
+github.com/pact-foundation/pact-go v1.0.4/go.mod h1:uExwJY4kCzNPcHRj+hCR/HBbOOIwwtUjcrb0b5/5kLM=
+github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
+github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
+github.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac=
+github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc=
+github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw=
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/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
+github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA=
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/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
+github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
+github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs=
+github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
+github.com/prometheus/client_golang v1.3.0/go.mod h1:hJaj2vgQTGQmVCsAACORcieXFeDPbaTKGT+JTgUa3og=
+github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
+github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/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/client_model v0.1.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
+github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
+github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
+github.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt26CguLLsqA=
+github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
+github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
+github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
+github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
+github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
+github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.3.0 h1:RR9dF3JtopPvtkroDZuVD7qquD0bnHlKSqaQhgwt8yk=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
+github.com/rogpeppe/go-internal v1.3.2/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
+github.com/rogpeppe/go-internal v1.4.0/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rubenv/sql-migrate v0.0.0-20190717103323-87ce952f7079 h1:xPeaaIHjF9j8jbYQ5xdvLnFp+lpmGYFG1uBPtXNBHno=
github.com/rubenv/sql-migrate v0.0.0-20190717103323-87ce952f7079/go.mod h1:WS0rl9eEliYI8DPnr3TOwz4439pay+qNgzJoVya/DmY=
+github.com/rubenv/sql-migrate v0.0.0-20200616145509-8d140a17f351 h1:HXr/qUllAWv9riaI4zh2eXWKmCSDqVS/XH1MRHLKRwk=
+github.com/rubenv/sql-migrate v0.0.0-20200616145509-8d140a17f351/go.mod h1:DCgfY80j8GYL7MLEfvcpSFvjD0L5yZq/aZUJmhZklyg=
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/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
+github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E=
+github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
+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.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
+github.com/sirupsen/logrus v1.7.0 h1:ShrD1U9pZB12TX0cVy0DtePoCH97K8EtX+mg7ZARUtM=
+github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
+github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
+github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
+github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
+github.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY=
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.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
+github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
+github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw=
+github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw=
+github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI=
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/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
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/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
+github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
+github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
+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/ziutek/mymysql v1.5.4 h1:GB0qdRGsTwQSBVYuVShFBKaXSnSnYYC2d9knnE1LHFs=
github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0=
+go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
+go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg=
+go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
+go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
+go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
+go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
+go.uber.org/atomic v1.5.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/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=
+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-20181029021203-45a5f77698d3/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-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190621222207-cc06ce4a13d4 h1:ydJNl0ENAG67pFbB+9tfhiL2pYqLhfoaZFw/cjLhY4A=
golang.org/x/crypto v0.0.0-20190621222207-cc06ce4a13d4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4 h1:HuIa8hRrWRSrqYzx1qI49NNxhdi2PrY7gxVSq1JjLDc=
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad h1:DN0cp81fZ3njFcrLCytUHRSUkqBjfTo4Tx9RJTWs0EY=
+golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
+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-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
+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-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/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-20181201002055-351d144fa1fc/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-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/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-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
+golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+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/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
+golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
+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-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+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-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/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-20181122145206-62eef0e2fa9b/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-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-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190515120540-06a5c4944438/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c h1:+EXw7AwNOKzPFXMZ1yNjO40aWCh3PIquJB2fYlv9wcs=
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191220142924-d4481acd189f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210108172913-0df2131ae363 h1:wHn06sgWHMO1VsQ8F+KzDJx/JzqfsNLnc+oEi07qD7s=
+golang.org/x/sys v0.0.0-20210108172913-0df2131ae363/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
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/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/time v0.0.0-20191024005414-555d28b269f0/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-20180828015842-6cd1fcedba52/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-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
+golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190624180213-70d37148ca0c/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
+golang.org/x/tools v0.0.0-20191004055002-72853e10c5a3/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-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=
+google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk=
+google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
+google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
+google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.1 h1:QzqyMA1tlu6CgqCDUtU9V+ZKhLFT2dkJuANu5QaxI3I=
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
+google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
+google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
+google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
+google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
+google.golang.org/genproto v0.0.0-20190530194941-fb225487d101/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s=
+google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
+google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
+google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
+google.golang.org/grpc v1.20.0/go.mod h1:chYK+tFQF0nDUGJgXMSgLCQk3phJEuONr2DCgLDdAQM=
+google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
+google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
+google.golang.org/grpc v1.22.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
+google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
+google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
+google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
+gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk=
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
+gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
+gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o=
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df h1:n7WqCuqOuCbNr617RXOY0AWRXxgwEyPp2z+p0+hgMuE=
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df/go.mod h1:LRQQ+SO6ZHR7tOkpBDuZnXENFzX8qRjMDMyPD6BRkCw=
gopkg.in/gorp.v1 v1.7.2 h1:j3DWlAyGVv8whO7AcIWznQ2Yj7yJkn34B8s63GViAAw=
gopkg.in/gorp.v1 v1.7.2/go.mod h1:Wo3h+DBQZIxATwftsglhdD/62zRFPhGhTiu5jUJmCaw=
+gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
+gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
+gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
+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 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
+gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
+honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+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=
+sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o=
+sourcegraph.com/sourcegraph/appdash v0.0.0-20190731080439-ebfcffb1b5c0/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU=
diff --git a/jobs.go b/jobs.go
index 096afed..9f43902 100644
--- a/jobs.go
+++ b/jobs.go
@@ -1,23 +1,25 @@
/*
- Copyright 2017-2019 Jan Dittberner
+Copyright 2017-2021 Jan Dittberner
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this program except in compliance with the License.
- You may obtain a copy of the License at
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this program except in compliance with the License.
+You may obtain a copy of the License at
- http://www.apache.org/licenses/LICENSE-2.0
+ 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.
+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.
*/
+
package main
import (
- log "github.com/sirupsen/logrus"
"time"
+
+ log "github.com/sirupsen/logrus"
)
type Job interface {
@@ -29,30 +31,34 @@ type Job interface {
type jobIdentifier int
const (
- JobIdCloseDecisions jobIdentifier = iota
- JobIdRemindVotersJob
+ JobIDCloseDecisions jobIdentifier = iota
+ JobIDRemindVotersJob
)
var rescheduleChannel = make(chan jobIdentifier, 1)
func JobScheduler(quitChannel chan int) {
var jobs = map[jobIdentifier]Job{
- JobIdCloseDecisions: NewCloseDecisionsJob(),
- JobIdRemindVotersJob: NewRemindVotersJob(),
+ JobIDCloseDecisions: NewCloseDecisionsJob(),
+ JobIDRemindVotersJob: NewRemindVotersJob(),
}
+
log.Info("started job scheduler")
for {
select {
- case jobId := <-rescheduleChannel:
- job := jobs[jobId]
+ case jobID := <-rescheduleChannel:
+ job := jobs[jobID]
+
log.Infof("reschedule job %s", job)
job.Schedule()
case <-quitChannel:
for _, job := range jobs {
job.Stop()
}
+
log.Info("stop job scheduler")
+
return
}
}
@@ -65,27 +71,35 @@ type CloseDecisionsJob struct {
func NewCloseDecisionsJob() *CloseDecisionsJob {
job := &CloseDecisionsJob{}
job.Schedule()
+
return job
}
func (j *CloseDecisionsJob) Schedule() {
- var nextDue *time.Time
- nextDue, err := GetNextPendingDecisionDue()
+ var (
+ nextDue *time.Time
+ err error
+ )
+
+ nextDue, err = GetNextPendingDecisionDue()
if err != nil {
log.Error("Could not get next pending due date")
+
if j.timer != nil {
j.timer.Stop()
j.timer = nil
}
+
return
}
+
if nextDue == nil {
log.Info("no next planned execution of CloseDecisionsJob")
j.Stop()
} else {
nextDue := nextDue.Add(time.Second)
log.Infof("scheduling CloseDecisionsJob for %s", nextDue)
- when := nextDue.Sub(time.Now())
+ when := time.Until(nextDue)
if j.timer != nil {
j.timer.Reset(when)
} else {
@@ -103,11 +117,13 @@ func (j *CloseDecisionsJob) Stop() {
func (j *CloseDecisionsJob) Run() {
log.Debug("running CloseDecisionsJob")
+
err := CloseDecisions()
if err != nil {
log.Errorf("closing decisions %v", err)
}
- rescheduleChannel <- JobIdCloseDecisions
+
+ rescheduleChannel <- JobIDCloseDecisions
}
func (j *CloseDecisionsJob) String() string {
@@ -121,14 +137,18 @@ type RemindVotersJob struct {
func NewRemindVotersJob() *RemindVotersJob {
job := &RemindVotersJob{}
job.Schedule()
+
return job
}
func (j *RemindVotersJob) Schedule() {
year, month, day := time.Now().UTC().Date()
nextExecution := time.Date(year, month, day, 0, 0, 0, 0, time.UTC).AddDate(0, 0, 3)
+
log.Infof("scheduling RemindVotersJob for %s", nextExecution)
- when := nextExecution.Sub(time.Now())
+
+ when := time.Until(nextExecution)
+
if j.timer != nil {
j.timer.Reset(when)
} else {
@@ -145,22 +165,33 @@ func (j *RemindVotersJob) Stop() {
func (j *RemindVotersJob) Run() {
log.Info("running RemindVotersJob")
- defer func() { rescheduleChannel <- JobIdRemindVotersJob }()
- voters, err := GetReminderVoters()
+ defer func() { rescheduleChannel <- JobIDRemindVotersJob }()
+
+ var (
+ voters []Voter
+ err error
+ )
+
+ voters, err = GetReminderVoters()
if err != nil {
log.Errorf("problem getting voters %v", err)
+
return
}
- for _, voter := range *voters {
- decisions, err := FindUnvotedDecisionsForVoter(&voter)
+ var decisions []Decision
+
+ for i := range voters {
+ decisions, err = FindUnVotedDecisionsForVoter(&voters[i])
if err != nil {
log.Errorf("problem getting unvoted decisions: %v", err)
+
return
}
- if len(*decisions) > 0 {
- NotifyMailChannel <- &RemindVoterNotification{voter: voter, decisions: *decisions}
+
+ if len(decisions) > 0 {
+ NotifyMailChannel <- &RemindVoterNotification{voter: voters[i], decisions: decisions}
}
}
}
diff --git a/models.go b/models.go
index 2ce7db4..3647097 100644
--- a/models.go
+++ b/models.go
@@ -1,51 +1,54 @@
/*
- Copyright 2017-2020 Jan Dittberner
+Copyright 2017-2021 Jan Dittberner
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this program except in compliance with the License.
- You may obtain a copy of the License at
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this program except in compliance with the License.
+You may obtain a copy of the License at
- http://www.apache.org/licenses/LICENSE-2.0
+ 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.
+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.
*/
+
package main
import (
"database/sql"
+ "errors"
"fmt"
- "github.com/jmoiron/sqlx"
- "github.com/rubenv/sql-migrate"
"time"
- migrations "git.cacert.org/cacert-boardvoting/db"
+ "github.com/jmoiron/sqlx"
+ migrate "github.com/rubenv/sql-migrate"
log "github.com/sirupsen/logrus"
+
+ migrations "git.cacert.org/cacert-boardvoting/db"
)
type sqlKey int
const (
sqlLoadDecisions sqlKey = iota
- sqlLoadUnvotedDecisions
+ sqlLoadUnVotedDecisions
sqlLoadDecisionByTag
- sqlLoadDecisionById
+ sqlLoadDecisionByID
sqlLoadVoteCountsForDecision
sqlLoadVotesForDecision
sqlLoadEnabledVoterByEmail
sqlCountOlderThanDecision
- sqlCountOlderThanUnvotedDecision
+ sqlCountOlderThanUnVotedDecision
sqlCreateDecision
sqlUpdateDecision
sqlUpdateDecisionStatus
sqlSelectClosableDecisions
sqlGetNextPendingDecisionDue
sqlGetReminderVoters
- sqlFindUnvotedDecisionsForVoter
- sqlGetEnabledVoterById
+ sqlFindUnVotedDecisionsForVoter
+ sqlGetEnabledVoterByID
sqlCreateVote
sqlLoadVote
sqlGetVotersForProxy
@@ -59,7 +62,7 @@ FROM decisions
JOIN voters ON decisions.proponent=voters.id
ORDER BY proposed DESC
LIMIT 10 OFFSET 10 * $1`,
- sqlLoadUnvotedDecisions: `
+ sqlLoadUnVotedDecisions: `
SELECT decisions.id, decisions.tag, decisions.proponent, voters.name AS proposer, decisions.proposed, decisions.title,
decisions.content, decisions.votetype, decisions.status, decisions.due, decisions.modified
FROM decisions
@@ -73,7 +76,7 @@ SELECT decisions.id, decisions.tag, decisions.proponent, voters.name AS proposer
FROM decisions
JOIN voters ON decisions.proponent=voters.id
WHERE decisions.tag=$1;`,
- sqlLoadDecisionById: `
+ sqlLoadDecisionByID: `
SELECT decisions.id, decisions.tag, decisions.proponent, decisions.proposed, decisions.title, decisions.content,
decisions.votetype, decisions.status, decisions.due, decisions.modified
FROM decisions
@@ -91,14 +94,14 @@ FROM voters
JOIN emails ON voters.id=emails.voter
JOIN user_roles ON user_roles.voter_id=voters.id
WHERE emails.address=$1 AND user_roles.role='VOTER'`,
- sqlGetEnabledVoterById: `
+ sqlGetEnabledVoterByID: `
SELECT voters.id, voters.name, voters.reminder
FROM voters
JOIN user_roles ON user_roles.voter_id=voters.id
WHERE user_roles.role='VOTER' AND voters.id=$1`,
sqlCountOlderThanDecision: `
SELECT COUNT(*) > 0 FROM decisions WHERE proposed < $1`,
- sqlCountOlderThanUnvotedDecision: `
+ sqlCountOlderThanUnVotedDecision: `
SELECT COUNT(*) > 0 FROM decisions
WHERE proposed < $1 AND status=0 AND id NOT IN (SELECT decision FROM votes WHERE votes.voter=$2)`,
sqlCreateDecision: `
@@ -134,7 +137,7 @@ SELECT voters.id, voters.name, voters.reminder
FROM voters
JOIN user_roles ON user_roles.voter_id=voters.id
WHERE user_roles.role='VOTER' AND reminder!='' AND reminder IS NOT NULL`,
- sqlFindUnvotedDecisionsForVoter: `
+ sqlFindUnVotedDecisionsForVoter: `
SELECT tag, title, votetype, due
FROM decisions
WHERE status = 0 AND id NOT IN (SELECT decision FROM votes WHERE voter = $1)
@@ -152,9 +155,9 @@ type VoteType uint8
type VoteStatus int8
type Decision struct {
- Id int64
+ ID int64 `db:"id"`
Proposed time.Time
- ProponentId int64 `db:"proponent"`
+ ProponentID int64 `db:"proponent"`
Title string
Content string
Quorum int
@@ -167,7 +170,7 @@ type Decision struct {
}
type Voter struct {
- Id int64
+ ID int64 `db:"id"`
Name string
Reminder string // reminder email address
}
@@ -185,23 +188,36 @@ const (
voteTypeVeto = 1
)
+const (
+ voteTypeLabelMotion = "motion"
+ voteTypeLabelUnknown = "unknown"
+ voteTypeLabelVeto = "veto"
+)
+
func (v VoteType) String() string {
switch v {
case voteTypeMotion:
- return "motion"
+ return voteTypeLabelMotion
case voteTypeVeto:
- return "veto"
+ return voteTypeLabelVeto
default:
- return "unknown"
+ return voteTypeLabelUnknown
}
}
-func (v VoteType) QuorumAndMajority() (int, int) {
+func (v VoteType) QuorumAndMajority() (int, float32) {
+ const (
+ majorityDefault = 0.99
+ majorityMotion = 0.50
+ quorumDefault = 1
+ quorumMotion = 3
+ )
+
switch v {
case voteTypeMotion:
- return 3, 50
+ return quorumMotion, majorityMotion
default:
- return 1, 99
+ return quorumDefault, majorityDefault
}
}
@@ -253,36 +269,42 @@ func (v VoteStatus) String() string {
}
type Vote struct {
- DecisionId int64 `db:"decision"`
- VoterId int64 `db:"voter"`
+ DecisionID int64 `db:"decision"`
+ VoterID int64 `db:"voter"`
Vote VoteChoice
Voted time.Time
Notes string
}
-type dbHandler struct {
+type DbHandler struct {
db *sqlx.DB
}
-var db *dbHandler
+var db *DbHandler
+
+func NewDB(database *sql.DB) *DbHandler {
+ handler := &DbHandler{db: sqlx.NewDb(database, "sqlite3")}
-func NewDB(database *sql.DB) *dbHandler {
- handler := &dbHandler{db: sqlx.NewDb(database, "sqlite3")}
_, err := migrate.Exec(database, "sqlite3", migrations.Migrations(), migrate.Up)
if err != nil {
log.Panicf("running database migration failed: %v", err)
}
failedStatements := make([]string, 0)
+
for _, sqlStatement := range sqlStatements {
var stmt *sqlx.Stmt
+
stmt, err := handler.db.Preparex(sqlStatement)
if err != nil {
log.Errorf("error parsing statement %s: %s", sqlStatement, err)
failedStatements = append(failedStatements, sqlStatement)
}
+
+ // nolint:sqlclosecheck
_ = stmt.Close()
}
+
if len(failedStatements) > 0 {
log.Panicf("%d statements failed to prepare", len(failedStatements))
}
@@ -290,44 +312,48 @@ func NewDB(database *sql.DB) *dbHandler {
return handler
}
-func (d *dbHandler) Close() error {
+func (d *DbHandler) Close() error {
return d.db.Close()
}
-func (d *dbHandler) getPreparedNamedStatement(statementKey sqlKey) *sqlx.NamedStmt {
+func (d *DbHandler) getPreparedNamedStatement(statementKey sqlKey) *sqlx.NamedStmt {
statement, err := d.db.PrepareNamed(sqlStatements[statementKey])
if err != nil {
log.Panicf("Preparing statement failed: %v", err)
}
+
return statement
}
-func (d *dbHandler) getPreparedStatement(statementKey sqlKey) *sqlx.Stmt {
+func (d *DbHandler) getPreparedStatement(statementKey sqlKey) *sqlx.Stmt {
statement, err := d.db.Preparex(sqlStatements[statementKey])
if err != nil {
log.Panicf("Preparing statement failed: %v", err)
}
+
return statement
}
-func (v *Vote) Save() (err error) {
+func (v *Vote) Save() error {
insertVoteStmt := db.getPreparedNamedStatement(sqlCreateVote)
+
defer func() { _ = insertVoteStmt.Close() }()
+ var err error
+
if _, err = insertVoteStmt.Exec(v); err != nil {
- log.Errorf("saving vote failed: %v", err)
- return
+ return fmt.Errorf("saving vote failed: %w", err)
}
getVoteStmt := db.getPreparedStatement(sqlLoadVote)
+
defer func() { _ = getVoteStmt.Close() }()
- if err = getVoteStmt.Get(v, v.DecisionId, v.VoterId); err != nil {
- log.Errorf("getting inserted vote failed: %v", err)
- return
+ if err = getVoteStmt.Get(v, v.DecisionID, v.VoterID); err != nil {
+ return fmt.Errorf("getting inserted vote failed: %w", err)
}
- return
+ return nil
}
type VoteSums struct {
@@ -349,18 +375,20 @@ func (v *VoteSums) Percent() int {
if totalVotes == 0 {
return 0
}
+
return v.Ayes * 100 / totalVotes
}
-func (v *VoteSums) CalculateResult(quorum int, majority int) (status VoteStatus, reasoning string) {
+func (v *VoteSums) CalculateResult(quorum int, majority float32) (VoteStatus, string) {
if v.VoteCount() < quorum {
- status, reasoning = voteStatusDeclined, fmt.Sprintf("Needed quorum of %d has not been reached.", quorum)
- } else if (v.Ayes / v.TotalVotes()) < (majority / 100) {
- status, reasoning = voteStatusDeclined, fmt.Sprintf("Needed majority of %d%% has not been reached.", majority)
- } else {
- status, reasoning = voteStatusApproved, "Quorum and majority have been reached"
+ return voteStatusDeclined, fmt.Sprintf("Needed quorum of %d has not been reached.", quorum)
}
- return
+
+ if (float32(v.Ayes) / float32(v.TotalVotes())) < majority {
+ return voteStatusDeclined, fmt.Sprintf("Needed majority of %0.2f%% has not been reached.", majority)
+ }
+
+ return voteStatusApproved, "Quorum and majority have been reached"
}
type VoteForDisplay struct {
@@ -375,23 +403,26 @@ type DecisionForDisplay struct {
Votes []VoteForDisplay
}
-func FindDecisionForDisplayByTag(tag string) (decision *DecisionForDisplay, err error) {
+func FindDecisionForDisplayByTag(tag string) (*DecisionForDisplay, error) {
decisionStmt := db.getPreparedStatement(sqlLoadDecisionByTag)
+
defer func() { _ = decisionStmt.Close() }()
- decision = &DecisionForDisplay{}
+ decision := &DecisionForDisplay{}
+
+ var err error
+
if err = decisionStmt.Get(decision, tag); err != nil {
- if err == sql.ErrNoRows {
- decision = nil
- err = nil
- return
- } else {
- log.Errorf("getting motion %s failed: %v", tag, err)
- return
+ if errors.Is(err, sql.ErrNoRows) {
+ return nil, nil
}
+
+ return nil, fmt.Errorf("getting motion %s failed: %w", tag, err)
}
+
decision.VoteSums, err = decision.Decision.VoteSums()
- return
+
+ return decision, err
}
// FindDecisionsForDisplayOnPage loads a set of decisions from the database.
@@ -399,61 +430,78 @@ func FindDecisionForDisplayByTag(tag string) (decision *DecisionForDisplay, err
// This function uses OFFSET for pagination which is not a good idea for larger data sets.
//
// TODO: migrate to timestamp base pagination
-func FindDecisionsForDisplayOnPage(page int64, unvoted bool, voter *Voter) (decisions []*DecisionForDisplay, err error) {
+func FindDecisionsForDisplayOnPage(page int64, unVoted bool, voter *Voter) ([]*DecisionForDisplay, error) {
var decisionsStmt *sqlx.Stmt
- if unvoted && voter != nil {
- decisionsStmt = db.getPreparedStatement(sqlLoadUnvotedDecisions)
+
+ if unVoted && voter != nil {
+ decisionsStmt = db.getPreparedStatement(sqlLoadUnVotedDecisions)
} else {
decisionsStmt = db.getPreparedStatement(sqlLoadDecisions)
}
+
defer func() { _ = decisionsStmt.Close() }()
- var rows *sqlx.Rows
- if unvoted && voter != nil {
- rows, err = decisionsStmt.Queryx(voter.Id, page-1)
+ var (
+ rows *sqlx.Rows
+ err error
+ decisions []*DecisionForDisplay
+ )
+
+ if unVoted && voter != nil {
+ rows, err = decisionsStmt.Queryx(voter.ID, page-1)
} else {
rows, err = decisionsStmt.Queryx(page - 1)
}
+
if err != nil {
- log.Errorf("loading motions for page %d failed: %v", page, err)
- return
+ return nil, fmt.Errorf("loading motions for page %d failed: %w", page, err)
}
+
defer func() { _ = rows.Close() }()
for rows.Next() {
var d DecisionForDisplay
+
if err = rows.StructScan(&d); err != nil {
- log.Errorf("loading motions for page %d failed: %v", page, err)
- return
+ return nil, fmt.Errorf("loading motions for page %d failed: %w", page, err)
}
+
d.VoteSums, err = d.Decision.VoteSums()
+
if err != nil {
- return
+ return nil, err
}
+
decisions = append(decisions, &d)
}
- return
+
+ return decisions, nil
}
-func (d *Decision) VoteSums() (sums *VoteSums, err error) {
+func (d *Decision) VoteSums() (*VoteSums, error) {
votesStmt := db.getPreparedStatement(sqlLoadVoteCountsForDecision)
+
defer func() { _ = votesStmt.Close() }()
- voteRows, err := votesStmt.Queryx(d.Id)
+ voteRows, err := votesStmt.Queryx(d.ID)
if err != nil {
- log.Errorf("fetching vote sums for motion %s failed: %v", d.Tag, err)
- return
+ return nil, fmt.Errorf("fetching vote sums for motion %s failed: %w", d.Tag, err)
}
+
defer func() { _ = voteRows.Close() }()
- sums = &VoteSums{}
+ sums := &VoteSums{}
+
for voteRows.Next() {
- var vote VoteChoice
- var count int
+ var (
+ vote VoteChoice
+ count int
+ )
+
if err = voteRows.Scan(&vote, &count); err != nil {
- log.Errorf("fetching vote sums for motion %s failed: %v", d.Tag, err)
- return
+ return nil, fmt.Errorf("fetching vote sums for motion %s failed: %w", d.Tag, err)
}
+
switch vote {
case voteAye:
sums.Ayes = count
@@ -463,79 +511,86 @@ func (d *Decision) VoteSums() (sums *VoteSums, err error) {
sums.Abstains = count
}
}
- return
+
+ return sums, nil
}
func (d *DecisionForDisplay) LoadVotes() (err error) {
votesStmt := db.getPreparedStatement(sqlLoadVotesForDecision)
+
defer func() { _ = votesStmt.Close() }()
- err = votesStmt.Select(&d.Votes, d.Id)
+ err = votesStmt.Select(&d.Votes, d.ID)
if err != nil {
log.Errorf("selecting votes for motion %s failed: %v", d.Tag, err)
+
return
}
+
return
}
-func (d *Decision) OlderExists(unvoted bool, voter *Voter) (result bool, err error) {
+func (d *Decision) OlderExists(unvoted bool, voter *Voter) (bool, error) {
+ var result bool
+
if unvoted && voter != nil {
- olderStmt := db.getPreparedStatement(sqlCountOlderThanUnvotedDecision)
+ olderStmt := db.getPreparedStatement(sqlCountOlderThanUnVotedDecision)
+
defer func() { _ = olderStmt.Close() }()
- if err = olderStmt.Get(&result, d.Proposed, voter.Id); err != nil {
- log.Errorf("finding older motions than %s failed: %v", d.Tag, err)
- return
+ if err := olderStmt.Get(&result, d.Proposed, voter.ID); err != nil {
+ return false, fmt.Errorf("finding older motions than %s failed: %w", d.Tag, err)
}
} else {
olderStmt := db.getPreparedStatement(sqlCountOlderThanDecision)
+
defer func() { _ = olderStmt.Close() }()
- if err = olderStmt.Get(&result, d.Proposed); err != nil {
- log.Errorf("finding older motions than %s failed: %v", d.Tag, err)
- return
+ if err := olderStmt.Get(&result, d.Proposed); err != nil {
+ return false, fmt.Errorf("finding older motions than %s failed: %w", d.Tag, err)
}
}
- return
+ return result, nil
}
-func (d *Decision) Create() (err error) {
+func (d *Decision) Create() error {
insertDecisionStmt := db.getPreparedNamedStatement(sqlCreateDecision)
+
defer func() { _ = insertDecisionStmt.Close() }()
result, err := insertDecisionStmt.Exec(d)
if err != nil {
- log.Errorf("creating motion failed: %v", err)
- return
+ return fmt.Errorf("creating motion failed: %w", err)
}
- lastInsertId, err := result.LastInsertId()
+ decisionID, err := result.LastInsertId()
if err != nil {
- log.Errorf("getting id of inserted motion failed: %v", err)
- return
+ return fmt.Errorf("getting id of inserted motion failed: %w", err)
}
- rescheduleChannel <- JobIdCloseDecisions
+ rescheduleChannel <- JobIDCloseDecisions
+
+ getDecisionStmt := db.getPreparedStatement(sqlLoadDecisionByID)
- getDecisionStmt := db.getPreparedStatement(sqlLoadDecisionById)
defer func() { _ = getDecisionStmt.Close() }()
- err = getDecisionStmt.Get(d, lastInsertId)
+ err = getDecisionStmt.Get(d, decisionID)
if err != nil {
- log.Errorf("getting inserted motion failed: %v", err)
- return
+ return fmt.Errorf("getting inserted motion failed: %w", err)
}
- return
+ return nil
}
-func (d *Decision) LoadWithId() (err error) {
- getDecisionStmt := db.getPreparedStatement(sqlLoadDecisionById)
+func (d *Decision) LoadWithID() (err error) {
+ getDecisionStmt := db.getPreparedStatement(sqlLoadDecisionByID)
+
defer func() { _ = getDecisionStmt.Close() }()
- err = getDecisionStmt.Get(d, d.Id)
+ err = getDecisionStmt.Get(d, d.ID)
if err != nil {
log.Errorf("loading updated motion failed: %v", err)
+
return
}
@@ -544,93 +599,110 @@ func (d *Decision) LoadWithId() (err error) {
func (d *Decision) Update() (err error) {
updateDecisionStmt := db.getPreparedNamedStatement(sqlUpdateDecision)
+
defer func() { _ = updateDecisionStmt.Close() }()
result, err := updateDecisionStmt.Exec(d)
if err != nil {
log.Errorf("updating motion failed: %v", err)
+
return
}
+
affectedRows, err := result.RowsAffected()
if err != nil {
log.Error("Problem determining the affected rows")
+
return
} else if affectedRows != 1 {
log.Warningf("wrong number of affected rows: %d (1 expected)", affectedRows)
}
- rescheduleChannel <- JobIdCloseDecisions
- err = d.LoadWithId()
+ rescheduleChannel <- JobIDCloseDecisions
+
+ err = d.LoadWithID()
+
return
}
-func (d *Decision) UpdateStatus() (err error) {
+func (d *Decision) UpdateStatus() error {
updateStatusStmt := db.getPreparedNamedStatement(sqlUpdateDecisionStatus)
+
defer func() { _ = updateStatusStmt.Close() }()
result, err := updateStatusStmt.Exec(d)
if err != nil {
- log.Errorf("setting motion status failed: %v", err)
- return
+ return fmt.Errorf("setting motion status failed: %w", err)
}
+
affectedRows, err := result.RowsAffected()
if err != nil {
- log.Errorf("determining the affected rows failed: %v", err)
- return
+ return fmt.Errorf("determining the affected rows failed: %w", err)
} else if affectedRows != 1 {
log.Warningf("wrong number of affected rows: %d (1 expected)", affectedRows)
}
- rescheduleChannel <- JobIdCloseDecisions
- err = d.LoadWithId()
- return
+ rescheduleChannel <- JobIDCloseDecisions
+
+ err = d.LoadWithID()
+
+ return err
}
func (d *Decision) String() string {
- return fmt.Sprintf("%s %s (Id %d)", d.Tag, d.Title, d.Id)
+ return fmt.Sprintf("%s %s (ID %d)", d.Tag, d.Title, d.ID)
}
-func FindVoterByAddress(emailAddress string) (voter *Voter, err error) {
+func FindVoterByAddress(emailAddress string) (*Voter, error) {
findVoterStmt := db.getPreparedStatement(sqlLoadEnabledVoterByEmail)
+
defer func() { _ = findVoterStmt.Close() }()
- voter = &Voter{}
- if err = findVoterStmt.Get(voter, emailAddress); err != nil {
- if err != sql.ErrNoRows {
- log.Errorf("getting voter for address %s failed: %v", emailAddress, err)
- } else {
- err = nil
- voter = nil
+ voter := &Voter{}
+ if err := findVoterStmt.Get(voter, emailAddress); err != nil {
+ if !errors.Is(err, sql.ErrNoRows) {
+ return nil, fmt.Errorf("getting voter for address %s failed: %w", emailAddress, err)
}
+
+ voter = nil
}
- return
+
+ return voter, nil
}
func (d *Decision) Close() error {
quorum, majority := d.VoteType.QuorumAndMajority()
- var voteSums *VoteSums
- var err error
+ var (
+ voteSums *VoteSums
+ err error
+ )
if voteSums, err = d.VoteSums(); err != nil {
log.Errorf("getting vote sums failed: %v", err)
+
return err
}
+
var reasoning string
+
d.Status, reasoning = voteSums.CalculateResult(quorum, majority)
closeDecisionStmt := db.getPreparedNamedStatement(sqlUpdateDecisionStatus)
+
defer func() { _ = closeDecisionStmt.Close() }()
result, err := closeDecisionStmt.Exec(d)
if err != nil {
- log.Errorf("closing vote failed: %v", err)
- return err
+ return fmt.Errorf("closing vote failed: %w", err)
}
- if affectedRows, err := result.RowsAffected(); err != nil {
- log.Errorf("getting affected rows failed: %v", err)
- return err
- } else if affectedRows != 1 {
+
+ affectedRows, err := result.RowsAffected()
+ if err != nil {
+ return fmt.Errorf("getting affected rows failed: %w", err)
+ }
+
+ if affectedRows != 1 {
log.Warningf("wrong number of affected rows: %d (1 expected)", affectedRows)
}
@@ -641,110 +713,117 @@ func (d *Decision) Close() error {
return nil
}
-func CloseDecisions() (err error) {
+func CloseDecisions() error {
getClosableDecisionsStmt := db.getPreparedNamedStatement(sqlSelectClosableDecisions)
+
defer func() { _ = getClosableDecisionsStmt.Close() }()
decisions := make([]*Decision, 0)
+
rows, err := getClosableDecisionsStmt.Queryx(struct{ Now time.Time }{time.Now().UTC()})
if err != nil {
- log.Errorf("fetching closable decisions failed: %v", err)
- return
+ return fmt.Errorf("fetching closable decisions failed: %w", err)
}
+
defer func() { _ = rows.Close() }()
+
for rows.Next() {
decision := &Decision{}
if err = rows.StructScan(decision); err != nil {
- log.Errorf("scanning row failed: %v", err)
- return
+ return fmt.Errorf("scanning row failed: %w", err)
}
+
decisions = append(decisions, decision)
}
+
defer func() { _ = rows.Close() }()
for _, decision := range decisions {
log.Infof("found closable decision %s", decision.Tag)
+
if err = decision.Close(); err != nil {
- log.Errorf("closing decision %s failed: %s", decision.Tag, err)
- return
+ return fmt.Errorf("closing decision %s failed: %w", decision.Tag, err)
}
}
- return
+ return nil
}
-func GetNextPendingDecisionDue() (due *time.Time, err error) {
+func GetNextPendingDecisionDue() (*time.Time, error) {
getNextPendingDecisionDueStmt := db.getPreparedStatement(sqlGetNextPendingDecisionDue)
+
defer func() { _ = getNextPendingDecisionDueStmt.Close() }()
row := getNextPendingDecisionDueStmt.QueryRow()
- due = &time.Time{}
- if err = row.Scan(due); err != nil {
- if err == sql.ErrNoRows {
+ due := &time.Time{}
+ if err := row.Scan(due); err != nil {
+ if errors.Is(err, sql.ErrNoRows) {
log.Debug("No pending decisions")
+
return nil, nil
}
- log.Errorf("parsing result failed: %v", err)
- return nil, err
+
+ return nil, fmt.Errorf("parsing result failed: %w", err)
}
- return
+ return due, nil
}
-func GetReminderVoters() (voters *[]Voter, err error) {
+func GetReminderVoters() ([]Voter, error) {
getReminderVotersStmt := db.getPreparedStatement(sqlGetReminderVoters)
+
defer func() { _ = getReminderVotersStmt.Close() }()
- voterSlice := make([]Voter, 0)
+ var voters []Voter
- if err = getReminderVotersStmt.Select(&voterSlice); err != nil {
- log.Errorf("getting voters failed: %v", err)
- return
+ if err := getReminderVotersStmt.Select(&voters); err != nil {
+ return nil, fmt.Errorf("getting voters failed: %w", err)
}
- voters = &voterSlice
- return
+ return voters, nil
}
-func FindUnvotedDecisionsForVoter(voter *Voter) (decisions *[]Decision, err error) {
- findUnvotedDecisionsForVoterStmt := db.getPreparedStatement(sqlFindUnvotedDecisionsForVoter)
- defer func() { _ = findUnvotedDecisionsForVoterStmt.Close() }()
+func FindUnVotedDecisionsForVoter(voter *Voter) ([]Decision, error) {
+ findUnVotedDecisionsForVoterStmt := db.getPreparedStatement(sqlFindUnVotedDecisionsForVoter)
- decisionsSlice := make([]Decision, 0)
+ defer func() { _ = findUnVotedDecisionsForVoterStmt.Close() }()
- if err = findUnvotedDecisionsForVoterStmt.Select(&decisionsSlice, voter.Id); err != nil {
- log.Errorf("getting unvoted decisions failed: %v", err)
- return
+ var decisions []Decision
+
+ if err := findUnVotedDecisionsForVoterStmt.Select(&decisions, voter.ID); err != nil {
+ return nil, fmt.Errorf("getting unvoted decisions failed: %w", err)
}
- decisions = &decisionsSlice
- return
+ return decisions, nil
}
-func GetVoterById(id int64) (voter *Voter, err error) {
- getVoterByIdStmt := db.getPreparedStatement(sqlGetEnabledVoterById)
- defer func() { _ = getVoterByIdStmt.Close() }()
+func GetVoterByID(id int64) (*Voter, error) {
+ getVoterByIDStmt := db.getPreparedStatement(sqlGetEnabledVoterByID)
- voter = &Voter{}
- if err = getVoterByIdStmt.Get(voter, id); err != nil {
- log.Errorf("getting voter failed: %v", err)
- return
+ defer func() { _ = getVoterByIDStmt.Close() }()
+
+ voter := &Voter{}
+ if err := getVoterByIDStmt.Get(voter, id); err != nil {
+ return nil, fmt.Errorf("getting voter failed: %w", err)
}
- return
+ return voter, nil
}
func GetVotersForProxy(proxy *Voter) (voters *[]Voter, err error) {
getVotersForProxyStmt := db.getPreparedStatement(sqlGetVotersForProxy)
+
defer func() { _ = getVotersForProxyStmt.Close() }()
votersSlice := make([]Voter, 0)
- if err = getVotersForProxyStmt.Select(&votersSlice, proxy.Id); err != nil {
+ if err = getVotersForProxyStmt.Select(&votersSlice, proxy.ID); err != nil {
log.Errorf("Error getting voters for proxy failed: %v", err)
+
return
}
+
voters = &votersSlice
return
diff --git a/notifications.go b/notifications.go
index ed77573..49fc543 100644
--- a/notifications.go
+++ b/notifications.go
@@ -1,27 +1,30 @@
/*
- Copyright 2017-2019 Jan Dittberner
+Copyright 2017-2021 Jan Dittberner
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this program except in compliance with the License.
- You may obtain a copy of the License at
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this program except in compliance with the License.
+You may obtain a copy of the License at
- http://www.apache.org/licenses/LICENSE-2.0
+ 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.
+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.
*/
+
package main
import (
"bytes"
"fmt"
- "git.cacert.org/cacert-boardvoting/boardvoting"
+ "text/template"
+
"github.com/Masterminds/sprig"
"gopkg.in/gomail.v2"
- "text/template"
+
+ "git.cacert.org/cacert-boardvoting/boardvoting"
log "github.com/sirupsen/logrus"
)
@@ -37,7 +40,7 @@ type recipientData struct {
field, address, name string
}
-type notificationContent struct {
+type NotificationContent struct {
template string
data interface{}
subject string
@@ -46,32 +49,39 @@ type notificationContent struct {
}
type NotificationMail interface {
- GetNotificationContent() *notificationContent
+ GetNotificationContent() *NotificationContent
}
var NotifyMailChannel = make(chan NotificationMail, 1)
func MailNotifier(quitMailNotifier chan int) {
log.Info("Launched mail notifier")
+
for {
select {
case notification := <-NotifyMailChannel:
content := notification.GetNotificationContent()
+
mailText, err := buildMail(content.template, content.data)
if err != nil {
log.Errorf("building mail failed: %v", err)
+
continue
}
m := gomail.NewMessage()
m.SetAddressHeader("From", config.NotificationSenderAddress, "CAcert board voting system")
+
for _, recipient := range content.recipients {
m.SetAddressHeader(recipient.field, recipient.address, recipient.name)
}
+
m.SetHeader("Subject", content.subject)
+
for _, header := range content.headers {
m.SetHeader(header.name, header.value...)
}
+
m.SetBody("text/plain", mailText.String())
d := gomail.NewDialer(config.MailServer.Host, config.MailServer.Port, "", "")
@@ -80,6 +90,7 @@ func MailNotifier(quitMailNotifier chan int) {
}
case <-quitMailNotifier:
log.Info("Ending mail notifier")
+
return
}
}
@@ -90,6 +101,7 @@ func buildMail(templateName string, context interface{}) (mailText *bytes.Buffer
if err != nil {
return
}
+
t, err := template.New(templateName).Funcs(sprig.GenericFuncMap()).Parse(string(b))
if err != nil {
return
@@ -97,8 +109,10 @@ func buildMail(templateName string, context interface{}) (mailText *bytes.Buffer
mailText = bytes.NewBufferString("")
if err := t.Execute(mailText, context); err != nil {
- log.Errorf("Failed to execute template %s with context %+v: %v", templateName, context, err)
- return nil, err
+ return nil, fmt.Errorf(
+ "failed to execute template %s with context %+v: %w",
+ templateName, context, err,
+ )
}
return
@@ -122,6 +136,7 @@ func (n *decisionReplyBase) getHeaders() headerList {
headers = append(headers, headerData{
name: "In-Reply-To", value: []string{fmt.Sprintf("<%s>", n.decision.Tag)},
})
+
return headers
}
@@ -139,11 +154,12 @@ type notificationClosedDecision struct {
func NewNotificationClosedDecision(decision *Decision, voteSums *VoteSums, reasoning string) NotificationMail {
notification := ¬ificationClosedDecision{voteSums: *voteSums, reasoning: reasoning}
notification.decision = *decision
+
return notification
}
-func (n *notificationClosedDecision) GetNotificationContent() *notificationContent {
- return ¬ificationContent{
+func (n *notificationClosedDecision) GetNotificationContent() *NotificationContent {
+ return &NotificationContent{
template: "closed_motion_mail.txt",
data: struct {
*Decision
@@ -162,10 +178,11 @@ type NotificationCreateMotion struct {
voter Voter
}
-func (n *NotificationCreateMotion) GetNotificationContent() *notificationContent {
+func (n *NotificationCreateMotion) GetNotificationContent() *NotificationContent {
voteURL := fmt.Sprintf("%s/vote/%s", config.BaseURL, n.decision.Tag)
unvotedURL := fmt.Sprintf("%s/motions/?unvoted=1", config.BaseURL)
- return ¬ificationContent{
+
+ return &NotificationContent{
template: "create_motion_mail.txt",
data: struct {
*Decision
@@ -188,13 +205,15 @@ type notificationUpdateMotion struct {
func NewNotificationUpdateMotion(decision Decision, voter Voter) NotificationMail {
notification := notificationUpdateMotion{voter: voter}
notification.decision = decision
+
return ¬ification
}
-func (n *notificationUpdateMotion) GetNotificationContent() *notificationContent {
+func (n *notificationUpdateMotion) GetNotificationContent() *NotificationContent {
voteURL := fmt.Sprintf("%s/vote/%s", config.BaseURL, n.decision.Tag)
unvotedURL := fmt.Sprintf("%s/motions/?unvoted=1", config.BaseURL)
- return ¬ificationContent{
+
+ return &NotificationContent{
template: "update_motion_mail.txt",
data: struct {
*Decision
@@ -217,11 +236,12 @@ type notificationWithDrawMotion struct {
func NewNotificationWithDrawMotion(decision *Decision, voter *Voter) NotificationMail {
notification := ¬ificationWithDrawMotion{voter: *voter}
notification.decision = *decision
+
return notification
}
-func (n *notificationWithDrawMotion) GetNotificationContent() *notificationContent {
- return ¬ificationContent{
+func (n *notificationWithDrawMotion) GetNotificationContent() *NotificationContent {
+ return &NotificationContent{
template: "withdraw_motion_mail.txt",
data: struct {
*Decision
@@ -238,8 +258,8 @@ type RemindVoterNotification struct {
decisions []Decision
}
-func (n *RemindVoterNotification) GetNotificationContent() *notificationContent {
- return ¬ificationContent{
+func (n *RemindVoterNotification) GetNotificationContent() *NotificationContent {
+ return &NotificationContent{
template: "remind_voter_mail.txt",
data: struct {
Decisions []Decision
@@ -266,14 +286,21 @@ type notificationProxyVote struct {
justification string
}
-func NewNotificationProxyVote(decision *Decision, proxy *Voter, voter *Voter, vote *Vote, justification string) NotificationMail {
+func NewNotificationProxyVote(
+ decision *Decision,
+ proxy *Voter,
+ voter *Voter,
+ vote *Vote,
+ justification string,
+) NotificationMail {
notification := ¬ificationProxyVote{proxy: *proxy, voter: *voter, vote: *vote, justification: justification}
notification.decision = *decision
+
return notification
}
-func (n *notificationProxyVote) GetNotificationContent() *notificationContent {
- return ¬ificationContent{
+func (n *notificationProxyVote) GetNotificationContent() *NotificationContent {
+ return &NotificationContent{
template: "proxy_vote_mail.txt",
data: struct {
Proxy string
@@ -298,11 +325,12 @@ type notificationDirectVote struct {
func NewNotificationDirectVote(decision *Decision, voter *Voter, vote *Vote) NotificationMail {
notification := ¬ificationDirectVote{voter: *voter, vote: *vote}
notification.decision = *decision
+
return notification
}
-func (n *notificationDirectVote) GetNotificationContent() *notificationContent {
- return ¬ificationContent{
+func (n *notificationDirectVote) GetNotificationContent() *NotificationContent {
+ return &NotificationContent{
template: "direct_vote_mail.txt",
data: struct {
Vote VoteChoice