10 _ "github.com/mattn/go-sqlite3"
12 "github.com/jmoiron/sqlx"
13 "github.com/Masterminds/sprig"
22 list_decisions_sql = `
23 SELECT decisions.id, decisions.tag, decisions.proponent,
24 voters.name AS proposer, decisions.proposed, decisions.title,
25 decisions.content, decisions.votetype, decisions.status, decisions.due,
28 JOIN voters ON decisions.proponent=voters.id
29 ORDER BY proposed DESC
30 LIMIT 10 OFFSET 10 * ($1 - 1)`
32 SELECT decisions.id, decisions.tag, decisions.proponent,
33 voters.name AS proposer, decisions.proposed, decisions.title,
34 decisions.content, decisions.votetype, decisions.status, decisions.due,
37 JOIN voters ON decisions.proponent=voters.id
38 WHERE decisions.id=$1;`
40 SELECT voters.id, voters.name
42 JOIN emails ON voters.id=emails.voter
43 WHERE emails.address=$1 AND voters.enabled=1`
45 SELECT vote, COUNT(vote)
51 var logger *log.Logger
66 func (v VoteType) String() string {
68 case voteTypeMotion: return "motion"
69 case voteTypeVeto: return "veto"
70 default: return "unknown"
74 func (v VoteType) QuorumAndMajority() (int, int) {
76 case voteTypeMotion: return 3, 50
81 type VoteSums struct {
87 func (v *VoteSums) voteCount() int {
88 return v.Ayes + v.Nayes + v.Abstains
93 func (v VoteStatus) String() string {
95 case -1: return "declined"
96 case 0: return "pending"
97 case 1: return "approved"
98 case -2: return "withdrawn"
99 default: return "unknown"
105 func (v VoteKind) String() string {
107 case voteAye: return "Aye"
108 case voteNaye: return "Naye"
109 case voteAbstain: return "Abstain"
110 default: return "unknown"
119 type Decision struct {
137 func (d *Decision) parseVote(vote int, count int) {
153 func authenticateVoter(emailAddress string, voter *Voter) bool {
159 auth_stmt, err := db.Preparex(get_voter)
163 defer auth_stmt.Close()
165 err = auth_stmt.Get(voter, emailAddress)
169 if err != sql.ErrNoRows {
176 func redirectToMotionsHandler(w http.ResponseWriter, _ *http.Request) {
177 w.Header().Add("Location", "/motions")
178 w.WriteHeader(http.StatusMovedPermanently)
181 func renderTemplate(w http.ResponseWriter, tmpl string, context interface{}) {
182 t := template.New("motions.html")
183 t.Funcs(sprig.FuncMap())
184 t, err := t.ParseFiles(fmt.Sprintf("templates/%s.html", tmpl))
186 http.Error(w, err.Error(), http.StatusInternalServerError)
188 err = t.Execute(w, context)
190 http.Error(w, err.Error(), http.StatusInternalServerError)
194 func authenticateRequest(
195 w http.ResponseWriter, r *http.Request,
196 handler func(http.ResponseWriter, *http.Request, *Voter)) {
199 authLoop: for _, cert := range r.TLS.PeerCertificates {
200 var isClientCert = false
201 for _, extKeyUsage := range cert.ExtKeyUsage {
202 if extKeyUsage == x509.ExtKeyUsageClientAuth {
211 for _, emailAddress := range cert.EmailAddresses {
212 if authenticateVoter(emailAddress, &voter) {
219 w.WriteHeader(http.StatusForbidden)
220 renderTemplate(w, "denied", nil)
223 handler(w, r, &voter)
226 func motionsHandler(w http.ResponseWriter, _ *http.Request, voter *Voter) {
232 // $page = is_numeric($_REQUEST['page'])?$_REQUEST['page']:1;
234 motion_stmt, err := db.Preparex(list_decisions_sql)
235 votes_stmt, err := db.Preparex(vote_count_sql)
239 defer motion_stmt.Close()
240 defer votes_stmt.Close()
242 rows, err := motion_stmt.Queryx(1)
256 err := rows.StructScan(&d)
261 voteRows, err := votes_stmt.Queryx(d.Id)
266 for voteRows.Next() {
268 err = voteRows.Scan(&vote, &count)
273 d.parseVote(vote, count)
275 page.Decisions = append(page.Decisions, d)
280 renderTemplate(w, "motions", page)
283 func votersHandler(w http.ResponseWriter, _ *http.Request, voter *Voter) {
289 fmt.Fprintln(w, "Hello", voter.Name)
291 sqlStmt := "SELECT name, reminder FROM voters WHERE enabled=1"
293 rows, err := db.Query(sqlStmt)
299 fmt.Print("Enabled voters\n\n")
300 fmt.Printf("%-30s %-30s\n", "Name", "Reminder E-Mail address")
301 fmt.Printf("%s %s\n", strings.Repeat("-", 30), strings.Repeat("-", 30))
306 err = rows.Scan(&name, &reminder)
310 fmt.Printf("%-30s %s\n", name, reminder)
319 BoardMailAddress string `yaml:"board_mail_address"`
320 NoticeSenderAddress string `yaml:"notice_sender_address"`
321 DatabaseFile string `yaml:"database_file"`
322 ClientCACertificates string `yaml:"client_ca_certificates"`
323 ServerCert string `yaml:"server_certificate"`
324 ServerKey string `yaml:"server_key"`
328 logger = log.New(os.Stderr, "boardvoting: ", log.LstdFlags | log.LUTC)
330 var filename = "config.yaml"
331 if len(os.Args) == 2 {
332 filename = os.Args[1]
340 source, err = ioutil.ReadFile(filename)
344 err = yaml.Unmarshal(source, &config)
348 logger.Printf("read configuration %v", config)
350 db, err = sqlx.Open("sqlite3", config.DatabaseFile)
355 http.HandleFunc("/motions", func(w http.ResponseWriter, r *http.Request) {
356 authenticateRequest(w, r, motionsHandler)
358 http.HandleFunc("/voters", func(w http.ResponseWriter, r *http.Request) {
359 authenticateRequest(w, r, votersHandler)
361 http.HandleFunc("/static/", http.FileServer(http.Dir(".")).ServeHTTP)
362 http.HandleFunc("/", redirectToMotionsHandler)
364 // load CA certificates for client authentication
365 caCert, err := ioutil.ReadFile(config.ClientCACertificates)
369 caCertPool := x509.NewCertPool()
370 if !caCertPool.AppendCertsFromPEM(caCert) {
371 logger.Fatal("could not initialize client CA certificate pool")
374 // setup HTTPS server
375 tlsConfig := &tls.Config{
376 ClientCAs:caCertPool,
377 ClientAuth:tls.RequireAndVerifyClientCert,
379 tlsConfig.BuildNameToCertificate()
381 server := &http.Server{
386 err = server.ListenAndServeTLS(config.ServerCert, config.ServerKey)
388 logger.Fatal("ListenAndServerTLS: ", err)