Start notification job refactoring

main
Jan Dittberner 2 years ago
parent 933f21a43c
commit 01b95f2253

@ -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:

@ -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 {

@ -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
}

@ -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)
}

@ -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)
}

@ -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
}

@ -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}},
}
}

@ -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
}

@ -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
)

@ -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=

@ -0,0 +1,6 @@
package internal
import "embed"
//go:embed "mailtemplates"
var MailTemplates embed.FS

@ -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
}

@ -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")
}

@ -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)
}

@ -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")
}
Loading…
Cancel
Save