diff --git a/.golangci.yml b/.golangci.yml index 54140a9..05aa8eb 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -29,6 +29,8 @@ linters-settings: gomnd: ignore-functions: - 'strconv.*' + ignored-numbers: + - '-1,0,1,2,8' goimports: local-prefixes: git.cacert.org/cacert-boardvoting misspell: diff --git a/cmd/boardvoting/config.go b/cmd/boardvoting/config.go index de60fee..2adb35a 100644 --- a/cmd/boardvoting/config.go +++ b/cmd/boardvoting/config.go @@ -23,6 +23,7 @@ import ( "errors" "fmt" "io/ioutil" + "time" "github.com/gorilla/sessions" "gopkg.in/yaml.v2" @@ -31,30 +32,40 @@ import ( const ( cookieSecretMinLen = 32 csrfKeyLength = 32 - httpIdleTimeout = 5 - httpReadHeaderTimeout = 10 - httpReadTimeout = 10 - httpWriteTimeout = 60 + httpIdleTimeout = 5 * time.Second + httpReadHeaderTimeout = 10 * time.Second + httpReadTimeout = 10 * time.Second + httpWriteTimeout = 60 * time.Second sessionCookieName = "votesession" ) -type Config struct { - NoticeMailAddress string `yaml:"notice_mail_address"` - VoteNoticeMailAddress string `yaml:"vote_notice_mail_address"` +type mailConfig struct { + SMTPHost string `yaml:"host"` + SMTPPort int `yaml:"port"` NotificationSenderAddress string `yaml:"notification_sender_address"` - DatabaseFile string `yaml:"database_file"` - ClientCACertificates string `yaml:"client_ca_certificates"` - ServerCert string `yaml:"server_certificate"` - ServerKey string `yaml:"server_key"` - CookieSecret string `yaml:"cookie_secret"` - CsrfKey string `yaml:"csrf_key"` - BaseURL string `yaml:"base_url"` - HTTPAddress string `yaml:"http_address,omitempty"` - HTTPSAddress string `yaml:"https_address,omitempty"` - MailServer struct { - Host string `yaml:"host"` - Port int `yaml:"port"` - } `yaml:"mail_server"` +} + +type httpTimeoutConfig struct { + Idle time.Duration `yaml:"idle,omitempty"` + Read time.Duration `yaml:"read,omitempty"` + ReadHeader time.Duration `yaml:"read_header,omitempty"` + Write time.Duration `yaml:"write,omitempty"` +} + +type Config struct { + NoticeMailAddress string `yaml:"notice_mail_address"` + VoteNoticeMailAddress string `yaml:"vote_notice_mail_address"` + DatabaseFile string `yaml:"database_file"` + ClientCACertificates string `yaml:"client_ca_certificates"` + ServerCert string `yaml:"server_certificate"` + ServerKey string `yaml:"server_key"` + CookieSecret string `yaml:"cookie_secret"` + CsrfKey string `yaml:"csrf_key"` + BaseURL string `yaml:"base_url"` + HTTPAddress string `yaml:"http_address,omitempty"` + HTTPSAddress string `yaml:"https_address,omitempty"` + MailConfig mailConfig `yaml:"mail_server"` + Timeouts httpTimeoutConfig `yaml:"timeouts,omitempty"` } type configKey int @@ -73,6 +84,12 @@ func parseConfig(ctx context.Context, configFile string) (context.Context, error config := &Config{ HTTPAddress: "127.0.0.1:8000", HTTPSAddress: "127.0.0.1:8433", + Timeouts: httpTimeoutConfig{ + Idle: httpIdleTimeout, + ReadHeader: httpReadHeaderTimeout, + Read: httpReadTimeout, + Write: httpWriteTimeout, + }, } if err := yaml.Unmarshal(source, config); err != nil { diff --git a/cmd/boardvoting/handlers.go b/cmd/boardvoting/handlers.go index 73ba0a5..65d00b4 100644 --- a/cmd/boardvoting/handlers.go +++ b/cmd/boardvoting/handlers.go @@ -24,7 +24,7 @@ import ( func (app *application) motions(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/motions/" { - http.NotFound(w, r) + app.notFound(w) return } @@ -37,16 +37,14 @@ func (app *application) motions(w http.ResponseWriter, r *http.Request) { ts, err := template.ParseFiles(files...) if err != nil { - app.errorLog.Print(err.Error()) - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + app.serverError(w, err) return } err = ts.ExecuteTemplate(w, "base", nil) if err != nil { - app.errorLog.Print(err.Error()) - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + app.serverError(w, err) } } @@ -59,7 +57,7 @@ func (app *application) home(w http.ResponseWriter, r *http.Request) { if r.Method != "GET" && r.Method != "HEAD" { w.Header().Set("Allow", "GET,HEAD") - http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) + app.clientError(w, http.StatusMethodNotAllowed) return } diff --git a/cmd/boardvoting/helpers.go b/cmd/boardvoting/helpers.go new file mode 100644 index 0000000..6262ef1 --- /dev/null +++ b/cmd/boardvoting/helpers.go @@ -0,0 +1,40 @@ +/* +Copyright 2017-2022 CAcert Inc. +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. +*/ + +package main + +import ( + "fmt" + "net/http" + "runtime/debug" +) + +func (app *application) serverError(w http.ResponseWriter, err error) { + trace := fmt.Sprintf("%s\n%s", err.Error(), debug.Stack()) + + _ = app.errorLog.Output(2, trace) + + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) +} + +func (app *application) clientError(w http.ResponseWriter, status int) { + http.Error(w, http.StatusText(status), status) +} + +func (app *application) notFound(w http.ResponseWriter) { + app.clientError(w, http.StatusNotFound) +} diff --git a/cmd/boardvoting/jobs.go b/cmd/boardvoting/jobs.go new file mode 100644 index 0000000..6d56bfc --- /dev/null +++ b/cmd/boardvoting/jobs.go @@ -0,0 +1,201 @@ +/* +Copyright 2017-2022 CAcert Inc. +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. +*/ + +package main + +import ( + "log" + "time" + + "git.cacert.org/cacert-boardvoting/internal/models" +) + +func (app *application) SetupJobs() { + quitChannel := make(chan struct{}) + + go app.jobScheduler.Schedule(quitChannel) + + go func() { + for range app.ctx.Done() { + quitChannel <- struct{}{} + } + }() +} + +type Job interface { + Schedule() + Run() + Stop() +} + +type RemindVotersJob struct { + infoLog, errorLog *log.Logger + timer *time.Timer + voters *models.VoterModel + decisions *models.DecisionModel + notify chan NotificationMail + reschedule chan Job +} + +func (r *RemindVotersJob) Schedule() { + const reminderDays = 3 + + year, month, day := time.Now().UTC().Date() + + nextExecution := time.Date( + year, month, day, 0, 0, 0, 0, time.UTC, + ).AddDate(0, 0, reminderDays) + + r.infoLog.Printf("scheduling RemindVotersJob for %s", nextExecution) + + when := time.Until(nextExecution) + + if r.timer != nil { + r.timer.Reset(when) + + return + } + + r.timer = time.AfterFunc(when, r.Run) +} + +func (r *RemindVotersJob) Run() { + r.infoLog.Print("running RemindVotersJob") + + defer func(r *RemindVotersJob) { r.reschedule <- r }(r) + + var ( + voters []models.Voter + decisions []models.Decision + err error + ) + + voters, err = r.voters.GetReminderVoters() + if err != nil { + r.errorLog.Printf("problem getting voters: %v", err) + + return + } + + for _, voter := range voters { + decisions, err = r.decisions.FindUnVotedDecisionsForVoter(&voter) + if err != nil { + r.errorLog.Printf("problem getting unvoted decisions: %v", err) + + return + } + + if len(decisions) > 0 { + r.notify <- &RemindVoterNotification{voter: voter, decisions: decisions} + } + } +} + +func (r *RemindVotersJob) Stop() { + if r.timer != nil { + r.timer.Stop() + + r.timer = nil + } +} + +func (app *application) NewRemindVotersJob( + rescheduleChannel chan Job, + notificationChannel chan NotificationMail, +) Job { + return &RemindVotersJob{ + infoLog: app.infoLog, + errorLog: app.errorLog, + voters: app.voters, + decisions: app.decisions, + reschedule: rescheduleChannel, + notify: notificationChannel, + } +} + +type closeDecisionsJob struct{} + +func (c *closeDecisionsJob) Schedule() { + // TODO implement me + panic("implement me") +} + +func (c *closeDecisionsJob) Run() { + // TODO implement me + panic("implement me") +} + +func (c *closeDecisionsJob) Stop() { + // TODO implement me + panic("implement me") +} + +func NewCloseDecisionsJob() Job { + // TODO implement real job + + return &closeDecisionsJob{} +} + +type JobScheduler struct { + infoLogger *log.Logger + errorLogger *log.Logger + jobs []Job + rescheduleChannel chan Job +} + +func (app *application) NewJobScheduler() { + rescheduleChannel := make(chan Job, 1) + + app.jobScheduler = &JobScheduler{ + infoLogger: app.infoLog, + errorLogger: app.errorLog, + jobs: make([]Job, 0, 2), + rescheduleChannel: rescheduleChannel, + } + + app.jobScheduler.addJob(NewCloseDecisionsJob()) + app.jobScheduler.addJob(app.NewRemindVotersJob(rescheduleChannel, app.mailNotifier.notifyChannel)) +} + +func (js *JobScheduler) Schedule(quitChannel chan struct{}) { + for _, job := range js.jobs { + js.infoLogger.Printf("schedule job %v", job) + + job.Schedule() + } + + for { + select { + case job := <-js.rescheduleChannel: + js.infoLogger.Printf("reschedule job %v", job) + + job.Schedule() + case <-quitChannel: + for _, job := range js.jobs { + job.Stop() + } + + js.infoLogger.Print("stop job scheduler") + + return + } + } +} + +func (js *JobScheduler) addJob(job Job) { + js.jobs = append(js.jobs, job) +} diff --git a/cmd/boardvoting/main.go b/cmd/boardvoting/main.go index a93504c..cddaac3 100644 --- a/cmd/boardvoting/main.go +++ b/cmd/boardvoting/main.go @@ -20,16 +20,18 @@ package main import ( "context" + "database/sql" "flag" - "io/fs" + "fmt" "log" "net/http" "os" - "github.com/vearutop/statigz" - "github.com/vearutop/statigz/brotli" + "github.com/jmoiron/sqlx" + _ "github.com/mattn/go-sqlite3" - "git.cacert.org/cacert-boardvoting/ui" + "git.cacert.org/cacert-boardvoting/internal" + "git.cacert.org/cacert-boardvoting/internal/models" ) var ( @@ -40,6 +42,13 @@ var ( type application struct { errorLog, infoLog *log.Logger + voters *models.VoterModel + decisions *models.DecisionModel + ctx context.Context + jobScheduler *JobScheduler + mailNotifier *MailNotifier + mailConfig mailConfig + baseURL string } func main() { @@ -56,40 +65,67 @@ func main() { errorLog.Fatal(err) } - mux := http.NewServeMux() - - staticDir, _ := fs.Sub(ui.Files, "static") + config, err := GetConfig(ctx) + if err != nil { + errorLog.Fatal(err) + } - staticData, ok := staticDir.(fs.ReadDirFS) - if !ok { - errorLog.Fatal("could not use uiStaticDir as fs.ReadDirFS") + db, err := openDB(config.DatabaseFile) + if err != nil { + errorLog.Fatal(err) } - fileServer := statigz.FileServer(staticData, brotli.AddEncoding, statigz.EncodeOnInit) + defer func(db *sqlx.DB) { + _ = db.Close() + }(db) - mux.Handle("/static/", http.StripPrefix("/static", fileServer)) + if err != nil { + errorLog.Fatalf("could not setup decision model: %v", err) + } app := &application{ - errorLog: errorLog, - infoLog: infoLog, + errorLog: errorLog, + infoLog: infoLog, + decisions: &models.DecisionModel{DB: db, InfoLog: infoLog}, + voters: &models.VoterModel{DB: db}, + ctx: context.Background(), + mailConfig: config.MailConfig, + baseURL: config.BaseURL, } + app.NewMailNotifier() + app.NewJobScheduler() - mux.HandleFunc("/", app.home) - mux.HandleFunc("/motions/", app.motions) - - config, err := GetConfig(ctx) + err = internal.InitializeDb(db.DB, infoLog) if err != nil { errorLog.Fatal(err) } srv := &http.Server{ - Addr: config.HTTPAddress, - ErrorLog: errorLog, - Handler: mux, + Addr: config.HTTPAddress, + ErrorLog: errorLog, + Handler: app.routes(), + IdleTimeout: config.Timeouts.Idle, + ReadHeaderTimeout: config.Timeouts.ReadHeader, + ReadTimeout: config.Timeouts.Read, + WriteTimeout: config.Timeouts.Write, } infoLog.Printf("Starting server on %s", config.HTTPAddress) err = srv.ListenAndServe() + errorLog.Fatal(err) } + +func openDB(dbFile string) (*sqlx.DB, error) { + db, err := sql.Open("sqlite3", dbFile) + if err != nil { + return nil, fmt.Errorf("could not open database file %s: %w", dbFile, err) + } + + if err = db.Ping(); err != nil { + return nil, fmt.Errorf("could not ping database: %w", err) + } + + return sqlx.NewDb(db, "sqlite3"), nil +} diff --git a/cmd/boardvoting/notifications.go b/cmd/boardvoting/notifications.go new file mode 100644 index 0000000..8952627 --- /dev/null +++ b/cmd/boardvoting/notifications.go @@ -0,0 +1,133 @@ +package main + +import ( + "bytes" + "fmt" + "path" + "text/template" + + "git.cacert.org/cacert-boardvoting/internal" + "git.cacert.org/cacert-boardvoting/internal/models" + "github.com/Masterminds/sprig/v3" + "gopkg.in/mail.v2" +) + +type headerData struct { + name string + value []string +} + +type recipientData struct { + field, address, name string +} + +type NotificationContent struct { + template string + data interface{} + subject string + headers []headerData + recipients []recipientData +} + +type NotificationMail interface { + GetNotificationContent() *NotificationContent +} + +type MailNotifier struct { + notifyChannel chan NotificationMail + senderAddress string + dialer *mail.Dialer +} + +func (app *application) NewMailNotifier() { + app.mailNotifier = &MailNotifier{ + notifyChannel: make(chan NotificationMail, 1), + senderAddress: app.mailConfig.NotificationSenderAddress, + dialer: mail.NewDialer(app.mailConfig.SMTPHost, app.mailConfig.SMTPPort, "", ""), + } +} + +func (app *application) StartMailNotifier() { + app.infoLog.Print("Launching mail notifier") + + for { + select { + case notification := <-app.mailNotifier.notifyChannel: + content := notification.GetNotificationContent() + + mailText, err := content.buildMail(app.baseURL) + if err != nil { + app.errorLog.Printf("building mail failed: %v", err) + + continue + } + + m := mail.NewMessage() + m.SetAddressHeader("From", app.mailNotifier.senderAddress, "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()) + + if err = app.mailNotifier.dialer.DialAndSend(m); err != nil { + app.errorLog.Printf("sending mail failed: %v", err) + } + case <-app.ctx.Done(): + app.infoLog.Print("ending mail notifier") + + return + } + } +} + +func (n *NotificationContent) buildMail(baseUrl string) (fmt.Stringer, error) { + b, err := internal.MailTemplates.ReadFile( + fmt.Sprintf(path.Join("mailtemplates", n.template)), + ) + if err != nil { + return nil, fmt.Errorf("could not read mail template %s: %w", n.template, err) + } + + t, err := template.New(n.template).Funcs(sprig.GenericFuncMap()).Parse(string(b)) + if err != nil { + return nil, fmt.Errorf("could not parse mail template %s: %w", n.template, err) + } + + data := struct { + Data any + BaseURL string + }{Data: n.data, BaseURL: baseUrl} + + mailText := bytes.NewBuffer(make([]byte, 0)) + if err = t.Execute(mailText, data); err != nil { + return nil, fmt.Errorf( + "failed to execute template %s with context %v: %w", n.template, n.data, err) + } + + return mailText, nil +} + +type RemindVoterNotification struct { + voter models.Voter + decisions []models.Decision +} + +func (r RemindVoterNotification) GetNotificationContent() *NotificationContent { + return &NotificationContent{ + template: "remind_voter_mail.txt", + data: struct { + Decisions []models.Decision + Name string + }{Decisions: r.decisions, Name: r.voter.Name}, + subject: "Outstanding CAcert board votes", + recipients: []recipientData{{"To", r.voter.Reminder, r.voter.Name}}, + } +} diff --git a/cmd/boardvoting/routes.go b/cmd/boardvoting/routes.go new file mode 100644 index 0000000..e6f8857 --- /dev/null +++ b/cmd/boardvoting/routes.go @@ -0,0 +1,48 @@ +/* +Copyright 2017-2022 CAcert Inc. +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. +*/ + +package main + +import ( + "io/fs" + "net/http" + + "github.com/vearutop/statigz" + "github.com/vearutop/statigz/brotli" + + "git.cacert.org/cacert-boardvoting/ui" +) + +func (app *application) routes() *http.ServeMux { + mux := http.NewServeMux() + + staticDir, _ := fs.Sub(ui.Files, "static") + + staticData, ok := staticDir.(fs.ReadDirFS) + if !ok { + app.errorLog.Fatal("could not use uiStaticDir as fs.ReadDirFS") + } + + fileServer := statigz.FileServer(staticData, brotli.AddEncoding, statigz.EncodeOnInit) + + mux.Handle("/static/", http.StripPrefix("/static", fileServer)) + + mux.HandleFunc("/", app.home) + mux.HandleFunc("/motions/", app.motions) + + return mux +} diff --git a/go.mod b/go.mod index 64258bd..d3ec881 100644 --- a/go.mod +++ b/go.mod @@ -25,14 +25,19 @@ require ( gopkg.in/yaml.v2 v2.4.0 ) +require github.com/stretchr/testify v1.7.0 + require ( github.com/Masterminds/goutils v1.1.1 // indirect github.com/Masterminds/semver/v3 v3.1.1 // indirect github.com/andybalholm/brotli v1.0.4 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect github.com/gorilla/securecookie v1.1.1 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/shopspring/decimal v1.3.1 // indirect github.com/spf13/cast v1.4.1 // indirect go.uber.org/atomic v1.9.0 // indirect + gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect ) diff --git a/go.sum b/go.sum index 17ebca2..6e21654 100644 --- a/go.sum +++ b/go.sum @@ -123,8 +123,6 @@ github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRF github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= github.com/alexflint/go-filemutex v0.0.0-20171022225611-72bdc8eae2ae/go.mod h1:CgnQgUtFrFz9mxFNtED3jI5tLDjKlOM+oUF/sTk6ps0= github.com/alexflint/go-filemutex v1.1.0/go.mod h1:7P4iRhttt/nUvUOrYIhcpMzv2G6CY9UnI16Z+UJqRyk= -github.com/andybalholm/brotli v1.0.1 h1:KqhlKozYbRtJvsPrrEeXcO+N2l6NYT5A2QAFmSULpEc= -github.com/andybalholm/brotli v1.0.1/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y= github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY= github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= @@ -178,8 +176,6 @@ github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJm github.com/blang/semver v3.1.0+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= -github.com/bool64/dev v0.1.18 h1:uMN5MsHrVWmtRoauefI8wD86b8vbXYnrCZlIhFGyuXI= -github.com/bool64/dev v0.1.18/go.mod h1:cTHiTDNc8EewrQPy3p1obNilpMpdmlUesDkFTF2zRWU= github.com/bool64/dev v0.2.9 h1:efyGf5pgx4CYWQpCzPEX8a1PgewaCGaEexXa+IYHT/8= github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= github.com/bshuster-repo/logrus-logstash-hook v0.4.1/go.mod h1:zsTqEiSzDgAa/8GZR7E1qaXrhYNDKBYy5/dWPTIflbk= @@ -461,8 +457,8 @@ github.com/go-openapi/swag v0.19.2/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= github.com/go-openapi/swag v0.19.14/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= -github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs= github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= @@ -511,7 +507,6 @@ github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXP github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= github.com/golang-jwt/jwt/v4 v4.1.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= -github.com/golang-migrate/migrate/v4 v4.14.2-0.20201125065321-a53e6fc42574 h1:YEVMe8861NCZxTMzTI5BCobpwGpt1Md6D8v00jDc68w= github.com/golang-migrate/migrate/v4 v4.14.2-0.20201125065321-a53e6fc42574/go.mod h1:l7Ks0Au6fYHuUIxUhQ0rcVX1uLlJg54C/VvW7tvxSz0= github.com/golang-migrate/migrate/v4 v4.15.2 h1:vU+M05vs6jWHKDdmE1Ecwj0BznygFc4QsdRe2E/L7kc= github.com/golang-migrate/migrate/v4 v4.15.2/go.mod h1:f2toGLkYqD3JH+Todi4aZ2ZdbeUNx4sIwiOK96rE9Lw= @@ -602,7 +597,6 @@ github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm4 github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.2.0 h1:qJYtXnJRWmpe7m/3XlyhrsLrEURqHRM2kxzoxXqyUDs= github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -615,8 +609,6 @@ github.com/googleapis/gnostic v0.5.1/go.mod h1:6U4PtQXGIEt/Z3h5MAT7FNofLnw9vXk2c github.com/googleapis/gnostic v0.5.5/go.mod h1:7+EbHbldMins07ALC74bsA81Ovc97DwqyJO1AENw9kA= 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.7.0 h1:mMPjV5/3Zd460xCavIkppUdvnl5fPXMpv2uz2Zyg7/Y= -github.com/gorilla/csrf v1.7.0/go.mod h1:+a/4tCmqhG6/w4oafeAZ9pEa3/NZOWYVbD9fV0FwIQA= github.com/gorilla/csrf v1.7.1 h1:Ir3o2c1/Uzj6FBxMlAUB6SivgVMy1ONXwYgXn+/aHPE= github.com/gorilla/csrf v1.7.1/go.mod h1:+a/4tCmqhG6/w4oafeAZ9pEa3/NZOWYVbD9fV0FwIQA= github.com/gorilla/handlers v0.0.0-20150720190736-60c7bfde3e33/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ= @@ -737,13 +729,10 @@ github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks= -github.com/jmoiron/sqlx v1.3.1 h1:aLN7YINNZ7cYOPK3QC83dbM6KT0NMqVMw961TqrejlE= github.com/jmoiron/sqlx v1.3.1/go.mod h1:2BljVx/86SuTyjE+aPYlHCTNvZrnJXghYGpNiXLBMCQ= github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g= github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ= github.com/joefitzgerald/rainbow-reporter v0.1.0/go.mod h1:481CNgqmVHQZzdIbN52CupLJyoVwB10FQ/IQlF1pdL8= -github.com/johejo/golang-migrate-extra v0.0.0-20210217013041-51a992e50d16 h1:sDjlyV4OzJYR2ZdLAAKZwV6N/CcX60xbGnPZzKJOZ50= -github.com/johejo/golang-migrate-extra v0.0.0-20210217013041-51a992e50d16/go.mod h1:lzH77MbyyahK7YO90wGRb65i9xLSoy2fD0dUSm23yMs= github.com/johejo/golang-migrate-extra v0.0.0-20211005021153-c17dd75f8b4a h1:89hRqHzTmEoJi8TY11K42F0isvNR0UAhL4V3hYD74pk= github.com/johejo/golang-migrate-extra v0.0.0-20211005021153-c17dd75f8b4a/go.mod h1:lzH77MbyyahK7YO90wGRb65i9xLSoy2fD0dUSm23yMs= github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= @@ -784,22 +773,21 @@ github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxv github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= 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/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA= github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= -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/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/ktrysmt/go-bitbucket v0.6.4/go.mod h1:9u0v3hsd2rqCHRIpbir1oP7F58uo5dq19sBYvuMoyQ4= github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= -github.com/lib/pq v1.8.0 h1:9xohqzkUwzR4Ga4ivdTcawVS89YSDVxXMa3xJX3cGzg= github.com/lib/pq v1.8.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lib/pq v1.10.0 h1:Zx5DJFEYQXio93kgXnQ09fXNiUKsqv4OUEu2UtGcB1E= github.com/lib/pq v1.10.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= @@ -834,7 +822,6 @@ github.com/mattn/go-shellwords v1.0.6/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vq github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y= github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= github.com/mattn/go-sqlite3 v1.10.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/mattn/go-sqlite3 v1.14.10/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= github.com/mattn/go-sqlite3 v1.14.12 h1:TJ1bhYJPV44phC+IMu1u2K/i5RriLTPe+yc68XDJ1Z0= @@ -847,8 +834,6 @@ github.com/miekg/pkcs11 v1.0.3/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WT github.com/mistifyio/go-zfs v2.1.2-0.20190413222219-f784269be439+incompatible/go.mod h1:8AuVvqP/mXw1px98n46wfvcGfQ4ci2FwoAjKYxuo3Z4= github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= -github.com/mitchellh/copystructure v1.1.1 h1:Bp6x9R1Wn16SIz3OfeDr0b7RnCG2OB66Y7PQyC/cvq4= -github.com/mitchellh/copystructure v1.1.1/go.mod h1:EBArHfARyrSWO/+Wyr9zwEkc6XMFB9XyNgFNmRkZZU4= github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= @@ -861,8 +846,6 @@ github.com/mitchellh/mapstructure v0.0.0-20180220230111-00c29f56e238/go.mod h1:F github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/osext v0.0.0-20151018003038-5e2d6d41470f/go.mod h1:OkQIRizQZAeMln+1tSwduZz7+Af5oFlKirV/MSYes2A= 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/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/moby/locker v1.0.1/go.mod h1:S7SDdo5zpBK84bzzVlKr2V0hz+7x9hWbYC/kq7oQppc= @@ -1026,7 +1009,6 @@ github.com/seccomp/libseccomp-golang v0.9.1/go.mod h1:GbW5+tmTXfcxTToHLXlScSlAvW github.com/seccomp/libseccomp-golang v0.9.2-0.20210429002308-3879420cc921/go.mod h1:JA8cRccbGaA1s33RQf7Y1+q9gHmZX1yB/z9WDN1C6fg= github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= github.com/shopspring/decimal v0.0.0-20200227202807-02e2044944cc/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= -github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ= github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8= github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= @@ -1055,7 +1037,6 @@ github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTd github.com/spf13/afero v1.3.3/go.mod h1:5KUK8ByomD5Ti5Artl0RtHeI5pTF7MIDuXL3yY520V4= github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= -github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng= github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cast v1.4.1 h1:s0hze+J0196ZfEMTs80N7UlFt0BDuQ7Q+JDnHiMWKdA= github.com/spf13/cast v1.4.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= @@ -1082,9 +1063,9 @@ github.com/stretchr/testify v1.2.0/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/syndtr/gocapability v0.0.0-20170704070218-db04d3cc01c8/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= @@ -1102,8 +1083,6 @@ github.com/urfave/cli v0.0.0-20171014202726-7bc6a0acffa5/go.mod h1:70zkFmudgCuE/ 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/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= -github.com/vearutop/statigz v1.1.2 h1:ltPbIR88z+6fNrF74WRbG59HVSdKA+Lr1dbn/wLcu4I= -github.com/vearutop/statigz v1.1.2/go.mod h1:q0L+aIjEv8qKU2WUV+legzbWLlNuP/x89qzMbmQCvLc= github.com/vearutop/statigz v1.1.8 h1:IJgQHx6EomuYOYd2TzFt3haP+BIzV471zn7aepRiLHA= github.com/vearutop/statigz v1.1.8/go.mod h1:pfzrpvgLRnFeSVZd9iUYrpYDLqbV+RgeCfizr3ZFf44= github.com/vishvananda/netlink v0.0.0-20181108222139-023a6dafdcdf/go.mod h1:+SR5DhBJrl6ZM7CoCKvpw5BKroDKQ+PJqOg65H/2ktk= @@ -1220,8 +1199,6 @@ golang.org/x/crypto v0.0.0-20200709230013-948cd5f35899/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= -golang.org/x/crypto v0.0.0-20210314154223-e6e6c4f2bb5b h1:wSOdpTq0/eI46Ez/LkDwIsAKA71YP2SRKBODiRWM0as= -golang.org/x/crypto v0.0.0-20210314154223-e6e6c4f2bb5b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= @@ -1463,8 +1440,6 @@ golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210304124612-50617c2ba197/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210313202042-bd2e13477e9c h1:coiPEfMv+ThsjULRDygLrJVlNE1gDdL2g65s0LhV2os= -golang.org/x/sys v0.0.0-20210313202042-bd2e13477e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210324051608-47abb6519492/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -1786,10 +1761,10 @@ gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gG gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20141024133853-64131543e789/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/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/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= @@ -1819,6 +1794,7 @@ gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gorm.io/driver/postgres v1.0.8/go.mod h1:4eOzrI1MUfm6ObJU/UcmbXyiHSs8jSwH95G5P5dxcAg= gorm.io/gorm v1.20.12/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw= diff --git a/internal/mailtemplates.go b/internal/mailtemplates.go new file mode 100644 index 0000000..fef5098 --- /dev/null +++ b/internal/mailtemplates.go @@ -0,0 +1,6 @@ +package internal + +import "embed" + +//go:embed "mailtemplates" +var MailTemplates embed.FS diff --git a/boardvoting/templates/closed_motion_mail.txt b/internal/mailtemplates/closed_motion_mail.txt similarity index 100% rename from boardvoting/templates/closed_motion_mail.txt rename to internal/mailtemplates/closed_motion_mail.txt diff --git a/boardvoting/templates/create_motion_mail.txt b/internal/mailtemplates/create_motion_mail.txt similarity index 100% rename from boardvoting/templates/create_motion_mail.txt rename to internal/mailtemplates/create_motion_mail.txt diff --git a/boardvoting/templates/direct_vote_mail.txt b/internal/mailtemplates/direct_vote_mail.txt similarity index 100% rename from boardvoting/templates/direct_vote_mail.txt rename to internal/mailtemplates/direct_vote_mail.txt diff --git a/boardvoting/templates/proxy_vote_mail.txt b/internal/mailtemplates/proxy_vote_mail.txt similarity index 100% rename from boardvoting/templates/proxy_vote_mail.txt rename to internal/mailtemplates/proxy_vote_mail.txt diff --git a/boardvoting/templates/remind_voter_mail.txt b/internal/mailtemplates/remind_voter_mail.txt similarity index 100% rename from boardvoting/templates/remind_voter_mail.txt rename to internal/mailtemplates/remind_voter_mail.txt diff --git a/boardvoting/templates/update_motion_mail.txt b/internal/mailtemplates/update_motion_mail.txt similarity index 100% rename from boardvoting/templates/update_motion_mail.txt rename to internal/mailtemplates/update_motion_mail.txt diff --git a/boardvoting/templates/withdraw_motion_mail.txt b/internal/mailtemplates/withdraw_motion_mail.txt similarity index 100% rename from boardvoting/templates/withdraw_motion_mail.txt rename to internal/mailtemplates/withdraw_motion_mail.txt diff --git a/internal/migrations.go b/internal/migrations.go index a8ef35b..dda703c 100644 --- a/internal/migrations.go +++ b/internal/migrations.go @@ -17,7 +17,66 @@ limitations under the License. package internal -import "embed" +import ( + "database/sql" + "embed" + "errors" + "fmt" + "log" + + "github.com/golang-migrate/migrate/v4" + "github.com/golang-migrate/migrate/v4/database/sqlite3" + "github.com/johejo/golang-migrate-extra/source/iofs" +) //go:embed "migrations" var Migrations embed.FS + +type migrateLogger struct { + log *log.Logger + verbose bool +} + +func (m migrateLogger) Printf(format string, v ...interface{}) { + m.log.Printf(format, v...) +} + +func (m migrateLogger) Verbose() bool { + return m.verbose +} + +func (m migrateLogger) Print(s string) { + m.log.Print(s) +} + +func InitializeDb(db *sql.DB, log *log.Logger) error { + source, err := iofs.New(Migrations, "migrations") + if err != nil { + return fmt.Errorf("could not create migration source: %w", err) + } + + driver, err := sqlite3.WithInstance(db, &sqlite3.Config{}) + if err != nil { + return fmt.Errorf("could not create migration driver: %w", err) + } + + m, err := migrate.NewWithInstance("iofs", source, "sqlite3", driver) + if err != nil { + return fmt.Errorf("could not create migration instance: %w", err) + } + + m.Log = &migrateLogger{log, true} + + err = m.Up() + if err != nil { + if !errors.Is(err, migrate.ErrNoChange) { + return fmt.Errorf("running database migration failed: %w", err) + } + + log.Print("no database migrations required") + } else { + log.Print("applied database migrations") + } + + return nil +} diff --git a/internal/models/decisions.go b/internal/models/decisions.go new file mode 100644 index 0000000..1a42427 --- /dev/null +++ b/internal/models/decisions.go @@ -0,0 +1,213 @@ +/* +Copyright 2017-2022 CAcert Inc. +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. +*/ + +package models + +import ( + "fmt" + "log" + "time" + + "github.com/jmoiron/sqlx" +) + +type VoteType uint8 + +const unknownVariant = "unknown" + +const ( + VoteTypeMotion VoteType = 0 + VoteTypeVeto VoteType = 1 +) + +var voteTypeLabels = map[VoteType]string{ + VoteTypeMotion: "motion", + VoteTypeVeto: "veto", +} + +func (v VoteType) String() string { + if label, ok := voteTypeLabels[v]; ok { + return label + } + + return unknownVariant +} + +func (v VoteType) QuorumAndMajority() (int, float32) { + const ( + majorityDefault = 0.99 + majorityMotion = 0.50 + quorumDefault = 1 + quorumMotion = 3 + ) + + if v == VoteTypeMotion { + return quorumMotion, majorityMotion + } + + return quorumDefault, majorityDefault +} + +type VoteStatus int8 + +const ( + voteStatusDeclined VoteStatus = -1 + voteStatusPending VoteStatus = 0 + voteStatusApproved VoteStatus = 1 + voteStatusWithdrawn VoteStatus = -2 +) + +var voteStatusLabels = map[VoteStatus]string{ + voteStatusDeclined: "declined", + voteStatusPending: "pending", + voteStatusApproved: "approved", + voteStatusWithdrawn: "withdrawn", +} + +func (v VoteStatus) String() string { + if label, ok := voteStatusLabels[v]; ok { + return label + } + + return unknownVariant +} + +type VoteChoice int + +const ( + voteAye VoteChoice = 1 + voteNaye VoteChoice = -1 + voteAbstain VoteChoice = 0 +) + +var voteChoiceLabels = map[VoteChoice]string{ + voteAye: "aye", + voteNaye: "naye", + voteAbstain: "abstain", +} + +func (v VoteChoice) String() string { + if label, ok := voteChoiceLabels[v]; ok { + return label + } + + return unknownVariant +} + +type Decision struct { + ID int64 `db:"id"` + Proposed time.Time + Proponent int64 `db:"proponent"` + Title string + Content string + Quorum int + Majority int + Status VoteStatus + Due time.Time + Modified time.Time + Tag string + VoteType VoteType +} + +type DecisionModel struct { + DB *sqlx.DB + InfoLog *log.Logger +} + +// Create a new decision. +func (m *DecisionModel) Create( + proponent *Voter, + voteType VoteType, + title, content string, + proposed, due time.Time, +) (int64, error) { + d := &Decision{ + Proposed: proposed.UTC(), + Proponent: proponent.ID, + Title: title, + Content: content, + Due: due.UTC(), + VoteType: voteType, + } + + result, err := m.DB.NamedExec(`INSERT INTO decisions +(proposed, proponent, title, content, votetype, status, due, modified, tag) +VALUES (:proposed, :proponent, :title, :content, :votetype, :status, :due, :proposed, + 'm' || strftime('%Y%m%d', :proposed) || '.' || ( + SELECT COUNT(*)+1 AS num + FROM decisions + WHERE proposed BETWEEN date(:proposed) AND date(:proposed, '1 day') +))`, d) + if err != nil { + return 0, fmt.Errorf("creating motion failed: %w", err) + } + + id, err := result.LastInsertId() + if err != nil { + return 0, fmt.Errorf("could not get inserted decision id: %w", err) + } + + return id, nil +} + +func (m *DecisionModel) CloseDecisions() error { + rows, err := m.DB.NamedQuery(` +SELECT decisions.id, decisions.tag, decisions.proponent, decisions.proposed, decisions.title, decisions.content, + decisions.votetype, decisions.status, decisions.due, decisions.modified +FROM decisions +WHERE decisions.status=0 AND :now > due`, struct{ Now time.Time }{Now: time.Now().UTC()}) + if err != nil { + return fmt.Errorf("fetching closable decisions failed: %w", err) + } + + defer func(rows *sqlx.Rows) { + _ = rows.Close() + }(rows) + + decisions := make([]*Decision, 0) + + for rows.Next() { + decision := &Decision{} + if err = rows.StructScan(decision); err != nil { + return fmt.Errorf("scanning row failed: %w", err) + } + + if rows.Err() != nil { + return fmt.Errorf("row error: %w", err) + } + + decisions = append(decisions, decision) + } + + for _, decision := range decisions { + m.InfoLog.Printf("found closable decision %s", decision.Tag) + + if err = m.Close(decision.Tag); err != nil { + return fmt.Errorf("closing decision %s failed: %w", decision.Tag, err) + } + } + + return nil +} + +func (m *DecisionModel) Close(tag string) error { + panic("not implemented") +} + +func (m *DecisionModel) FindUnVotedDecisionsForVoter(v *Voter) ([]Decision, error) { + panic("not implemented") +} diff --git a/internal/models/decisions_test.go b/internal/models/decisions_test.go new file mode 100644 index 0000000..8ef7f9a --- /dev/null +++ b/internal/models/decisions_test.go @@ -0,0 +1,42 @@ +package models_test + +import ( + "database/sql" + "log" + "os" + "path" + "testing" + "time" + + "git.cacert.org/cacert-boardvoting/internal" + "git.cacert.org/cacert-boardvoting/internal/models" + "github.com/jmoiron/sqlx" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestDecisionModel_Create(t *testing.T) { + testDir := t.TempDir() + + db, err := sql.Open("sqlite3", path.Join(testDir, "test.sqlite")) + require.NoError(t, err) + + dbx := sqlx.NewDb(db, "sqlite3") + + logger := log.New(os.Stdout, "", log.LstdFlags) + + err = internal.InitializeDb(dbx.DB, logger) + require.NoError(t, err) + + dm := models.DecisionModel{DB: dbx, InfoLog: logger} + + v := &models.Voter{ + ID: 1, // sqlite does not check referential integrity. Might fail with a foreign key index. + Name: "test voter", + Reminder: "test+voter@example.com", + } + + id, err := dm.Create(v, models.VoteTypeMotion, "test motion", "I move that we should test more", time.Now(), time.Now().AddDate(0, 0, 7)) + assert.NoError(t, err) + assert.NotEmpty(t, id) +} diff --git a/internal/models/voters.go b/internal/models/voters.go new file mode 100644 index 0000000..8c544a7 --- /dev/null +++ b/internal/models/voters.go @@ -0,0 +1,36 @@ +/* +Copyright 2017-2022 CAcert Inc. +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. +*/ + +package models + +import ( + "github.com/jmoiron/sqlx" +) + +type Voter struct { + ID int64 `db:"id"` + Name string + Reminder string // reminder email address +} + +type VoterModel struct { + DB *sqlx.DB +} + +func (m VoterModel) GetReminderVoters() ([]Voter, error) { + panic("not implemented") +}