Switch to context API
authorJan Dittberner <jan@dittberner.info>
Mon, 17 Apr 2017 20:56:20 +0000 (22:56 +0200)
committerJan Dittberner <jan@dittberner.info>
Fri, 21 Apr 2017 22:12:24 +0000 (00:12 +0200)
actions.go
boardvoting.go
models.go

index fde767c..f41e1ff 100644 (file)
@@ -10,8 +10,8 @@ func WithdrawMotion(decision *Decision, voter *Voter) (err error) {
        // load template, fill name, tag, title, content
        type mailContext struct {
                *Decision
-               Name string
-               Sender string
+               Name      string
+               Sender    string
                Recipient string
        }
        context := mailContext{decision, voter.Name, config.NoticeSenderAddress, config.BoardMailAddress}
index 77499a2..1fe031e 100644 (file)
@@ -1,6 +1,7 @@
 package main
 
 import (
+       "context"
        "crypto/tls"
        "crypto/x509"
        "fmt"
@@ -35,7 +36,15 @@ func renderTemplate(w http.ResponseWriter, tmpl []string, context interface{}) {
        }
 }
 
-func authenticateRequest(w http.ResponseWriter, r *http.Request, authRequired bool, handler func(http.ResponseWriter, *http.Request, *Voter)) {
+type contextKey int
+
+const (
+       ctxNeedsAuth contextKey = iota
+       ctxVoter     contextKey = iota
+       ctxDecision  contextKey = iota
+)
+
+func authenticateRequest(w http.ResponseWriter, r *http.Request, handler func(http.ResponseWriter, *http.Request)) {
        for _, cert := range r.TLS.PeerCertificates {
                for _, extKeyUsage := range cert.ExtKeyUsage {
                        if extKeyUsage == x509.ExtKeyUsageClientAuth {
@@ -46,19 +55,20 @@ func authenticateRequest(w http.ResponseWriter, r *http.Request, authRequired bo
                                                return
                                        }
                                        if voter != nil {
-                                               handler(w, r, voter)
+                                               handler(w, r.WithContext(context.WithValue(r.Context(), ctxVoter, voter)))
                                                return
                                        }
                                }
                        }
                }
        }
-       if authRequired {
+       needsAuth, ok := r.Context().Value(ctxNeedsAuth).(bool)
+       if ok && needsAuth {
                w.WriteHeader(http.StatusForbidden)
                renderTemplate(w, []string{"denied.html"}, nil)
                return
        }
-       handler(w, r, nil)
+       handler(w, r)
 }
 
 type motionParameters struct {
@@ -66,7 +76,7 @@ type motionParameters struct {
 }
 
 type motionListParameters struct {
-       Page int64
+       Page  int64
        Flags struct {
                Confirmed, Withdraw, Unvoted bool
        }
@@ -96,74 +106,86 @@ func parseMotionListParameters(r *http.Request) motionListParameters {
        return m
 }
 
-func motionListHandler(w http.ResponseWriter, r *http.Request, voter *Voter) {
+func motionListHandler(w http.ResponseWriter, r *http.Request) {
        params := parseMotionListParameters(r)
 
-       var context struct {
+       var templateContext struct {
                Decisions          []*DecisionForDisplay
                Voter              *Voter
                Params             *motionListParameters
                PrevPage, NextPage int64
                PageTitle          string
        }
-       context.Voter = voter
-       context.Params = &params
+       if voter, ok := r.Context().Value(ctxVoter).(*Voter); ok {
+               templateContext.Voter = voter
+       }
+       templateContext.Params = &params
        var err error
 
-       if params.Flags.Unvoted {
-               if context.Decisions, err = FindVotersUnvotedDecisionsForDisplayOnPage(params.Page, voter); err != nil {
+       if params.Flags.Unvoted && templateContext.Voter != nil {
+               if templateContext.Decisions, err = FindVotersUnvotedDecisionsForDisplayOnPage(
+                       params.Page, templateContext.Voter); err != nil {
                        http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
                        return
                }
        } else {
-               if context.Decisions, err = FindDecisionsForDisplayOnPage(params.Page); err != nil {
+               if templateContext.Decisions, err = FindDecisionsForDisplayOnPage(params.Page); err != nil {
                        http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
                        return
                }
        }
 
-       if len(context.Decisions) > 0 {
-               olderExists, err := context.Decisions[len(context.Decisions)-1].OlderExists()
+       if len(templateContext.Decisions) > 0 {
+               olderExists, err := templateContext.Decisions[len(templateContext.Decisions)-1].OlderExists()
                if err != nil {
                        http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
                        return
                }
                if olderExists {
-                       context.NextPage = params.Page + 1
+                       templateContext.NextPage = params.Page + 1
                }
        }
 
        if params.Page > 1 {
-               context.PrevPage = params.Page - 1
+               templateContext.PrevPage = params.Page - 1
        }
 
-       renderTemplate(w, []string{"motions.html", "motion_fragments.html", "header.html", "footer.html"}, context)
+       renderTemplate(w, []string{"motions.html", "motion_fragments.html", "header.html", "footer.html"}, templateContext)
 }
 
-func motionHandler(w http.ResponseWriter, r *http.Request, voter *Voter, decision *DecisionForDisplay) {
+func motionHandler(w http.ResponseWriter, r *http.Request) {
        params := parseMotionParameters(r)
 
-       var context struct {
+       decision, ok := getDecisionFromRequest(r)
+       if !ok {
+               http.Error(w, http.StatusText(http.StatusPreconditionFailed), http.StatusPreconditionFailed)
+               return
+       }
+
+       var templateContext struct {
                Decision           *DecisionForDisplay
                Voter              *Voter
                Params             *motionParameters
                PrevPage, NextPage int64
                PageTitle          string
        }
-       context.Voter = voter
-       context.Params = &params
+       voter, ok := getVoterFromRequest(r)
+       if ok {
+               templateContext.Voter = voter
+       }
+       templateContext.Params = &params
        if params.ShowVotes {
                if err := decision.LoadVotes(); err != nil {
                        http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
                        return
                }
        }
-       context.Decision = decision
-       context.PageTitle = fmt.Sprintf("Motion %s: %s", decision.Tag, decision.Title)
-       renderTemplate(w, []string{"motion.html", "motion_fragments.html", "header.html", "footer.html"}, context)
+       templateContext.Decision = decision
+       templateContext.PageTitle = fmt.Sprintf("Motion %s: %s", decision.Tag, decision.Title)
+       renderTemplate(w, []string{"motion.html", "motion_fragments.html", "header.html", "footer.html"}, templateContext)
 }
 
-func singleDecisionHandler(w http.ResponseWriter, r *http.Request, v *Voter, tag string, handler func(http.ResponseWriter, *http.Request, *Voter, *DecisionForDisplay)) {
+func singleDecisionHandler(w http.ResponseWriter, r *http.Request, tag string, handler func(http.ResponseWriter, *http.Request)) {
        decision, err := FindDecisionForDisplayByTag(tag)
        if err != nil {
                http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
@@ -173,22 +195,45 @@ func singleDecisionHandler(w http.ResponseWriter, r *http.Request, v *Voter, tag
                http.NotFound(w, r)
                return
        }
-       handler(w, r, v, decision)
+       handler(w, r.WithContext(context.WithValue(r.Context(), ctxDecision, decision)))
 }
 
-type motionsHandler struct{}
-
 type motionActionHandler interface {
-       Handle(w http.ResponseWriter, r *http.Request, voter *Voter, decision *DecisionForDisplay)
+       Handle(w http.ResponseWriter, r *http.Request)
        NeedsAuth() bool
 }
 
-type withDrawMotionAction struct{}
+type authenticationRequiredHandler struct{}
 
-func (withDrawMotionAction) Handle(w http.ResponseWriter, r *http.Request, voter *Voter, decision *DecisionForDisplay) {
-       fmt.Fprintln(w, "Withdraw motion", decision.Tag)
-       // TODO: implement
-       if r.Method == http.MethodPost {
+func (authenticationRequiredHandler) NeedsAuth() bool {
+       return true
+}
+
+type withDrawMotionAction struct {
+       authenticationRequiredHandler
+}
+
+func getVoterFromRequest(r *http.Request) (voter *Voter, ok bool) {
+       voter, ok = r.Context().Value(ctxVoter).(*Voter)
+       return
+}
+
+func getDecisionFromRequest(r *http.Request) (decision *DecisionForDisplay, ok bool) {
+       decision, ok = r.Context().Value(ctxDecision).(*DecisionForDisplay)
+       return
+}
+
+func (withDrawMotionAction) Handle(w http.ResponseWriter, r *http.Request) {
+       voter, voter_ok := getVoterFromRequest(r)
+       decision, decision_ok := getDecisionFromRequest(r)
+
+       if !voter_ok || !decision_ok || decision.Status != voteStatusPending {
+               http.Error(w, http.StatusText(http.StatusPreconditionFailed), http.StatusPreconditionFailed)
+               return
+       }
+
+       switch r.Method {
+       case http.MethodPost:
                if confirm, err := strconv.ParseBool(r.PostFormValue("confirm")); err != nil {
                        log.Println("could not parse confirm parameter:", err)
                        http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
@@ -197,23 +242,28 @@ func (withDrawMotionAction) Handle(w http.ResponseWriter, r *http.Request, voter
                } else {
                        http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
                }
+               http.Redirect(w, r, "/motions/", http.StatusTemporaryRedirect)
+               return
+       default:
+               fmt.Fprintln(w, "Withdraw motion", decision.Tag)
        }
 }
 
-func (withDrawMotionAction) NeedsAuth() bool {
-       return true
+type editMotionAction struct {
+       authenticationRequiredHandler
 }
 
-type editMotionAction struct{}
-
-func (editMotionAction) Handle(w http.ResponseWriter, r *http.Request, voter *Voter, decision *DecisionForDisplay) {
+func (editMotionAction) Handle(w http.ResponseWriter, r *http.Request) {
+       decision, ok := getDecisionFromRequest(r)
+       if !ok || decision.Status != voteStatusPending {
+               http.Error(w, http.StatusText(http.StatusPreconditionFailed), http.StatusPreconditionFailed)
+               return
+       }
        fmt.Fprintln(w, "Edit motion", decision.Tag)
        // TODO: implement
 }
 
-func (editMotionAction) NeedsAuth() bool {
-       return true
-}
+type motionsHandler struct{}
 
 func (h motionsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
        if err := db.Ping(); err != nil {
@@ -224,12 +274,12 @@ func (h motionsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 
        var motionActionMap = map[string]motionActionHandler{
                "withdraw": withDrawMotionAction{},
-               "edit": editMotionAction{},
+               "edit":     editMotionAction{},
        }
 
        switch {
        case subURL == "":
-               authenticateRequest(w, r, false, motionListHandler)
+               authenticateRequest(w, r.WithContext(context.WithValue(r.Context(), ctxNeedsAuth, false)), motionListHandler)
                return
        case strings.Count(subURL, "/") == 1:
                parts := strings.Split(subURL, "/")
@@ -240,16 +290,18 @@ func (h motionsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
                        http.NotFound(w, r)
                        return
                }
-               authenticateRequest(w, r, action.NeedsAuth(), func(w http.ResponseWriter, r *http.Request, v *Voter) {
-                       singleDecisionHandler(w, r, v, motionTag, action.Handle)
-               })
-
+               authenticateRequest(
+                       w, r.WithContext(context.WithValue(r.Context(), ctxNeedsAuth, action.NeedsAuth())),
+                       func(w http.ResponseWriter, r *http.Request) {
+                               singleDecisionHandler(w, r, motionTag, action.Handle)
+                       })
                logger.Printf("motion: %s, action: %s\n", motionTag, action)
                return
        case strings.Count(subURL, "/") == 0:
-               authenticateRequest(w, r, false, func(w http.ResponseWriter, r *http.Request, v *Voter) {
-                       singleDecisionHandler(w, r, v, subURL, motionHandler)
-               })
+               authenticateRequest(w, r.WithContext(context.WithValue(r.Context(), ctxNeedsAuth, false)),
+                       func(w http.ResponseWriter, r *http.Request) {
+                               singleDecisionHandler(w, r, subURL, motionHandler)
+                       })
                return
        default:
                fmt.Fprintf(w, "No handler for '%s'", subURL)
@@ -257,8 +309,10 @@ func (h motionsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
        }
 }
 
-func newMotionHandler(w http.ResponseWriter, _ *http.Request, _ *Voter) {
-       fmt.Fprintln(w,"New motion")
+func newMotionHandler(w http.ResponseWriter, r *http.Request) {
+       fmt.Fprintln(w, "New motion")
+       voter, _ := getVoterFromRequest(r)
+       fmt.Fprintf(w, "%+v\n", voter)
        // TODO: implement
 }
 
@@ -301,7 +355,7 @@ func main() {
 
        http.Handle("/motions/", http.StripPrefix("/motions/", motionsHandler{}))
        http.HandleFunc("/newmotion/", func(w http.ResponseWriter, r *http.Request) {
-               authenticateRequest(w, r, true, newMotionHandler)
+               authenticateRequest(w, r.WithContext(context.WithValue(r.Context(), ctxNeedsAuth, true)), newMotionHandler)
        })
        http.Handle("/static/", http.FileServer(http.Dir(".")))
        http.Handle("/", http.RedirectHandler("/motions/", http.StatusMovedPermanently))
@@ -319,7 +373,7 @@ func main() {
        // setup HTTPS server
        tlsConfig := &tls.Config{
                ClientCAs:  caCertPool,
-               ClientAuth: tls.RequireAndVerifyClientCert,
+               ClientAuth: tls.VerifyClientCertIfGiven,
        }
        tlsConfig.BuildNameToCertificate()
 
index fe6cd23..46d669e 100644 (file)
--- a/models.go
+++ b/models.go
@@ -25,7 +25,7 @@ FROM decisions
 JOIN voters ON decisions.proponent=voters.id
 WHERE decisions.tag=$1;`
        sqlGetVoter = `
-SELECT voters.id, voters.name
+SELECT voters.id, voters.name, voters.enabled, voters.reminder
 FROM voters
 JOIN emails ON voters.id=emails.voter
 WHERE emails.address=$1 AND voters.enabled=1`
@@ -139,15 +139,22 @@ func (v VoteChoice) String() string {
        }
 }
 
+const (
+       voteStatusDeclined  = -1
+       voteStatusPending   = 0
+       voteStatusApproved  = 1
+       voteStatusWithdrawn = -2
+)
+
 func (v VoteStatus) String() string {
        switch v {
-       case -1:
+       case voteStatusDeclined:
                return "declined"
-       case 0:
+       case voteStatusPending:
                return "pending"
-       case 1:
+       case voteStatusApproved:
                return "approved"
-       case -2:
+       case voteStatusWithdrawn:
                return "withdrawn"
        default:
                return "unknown"
@@ -240,7 +247,7 @@ func FindVotersUnvotedDecisionsForDisplayOnPage(page int64, voter *Voter) (decis
        }
        defer decisionsStmt.Close()
 
-       rows, err := decisionsStmt.Queryx(page - 1, voter.Id)
+       rows, err := decisionsStmt.Queryx(page-1, voter.Id)
        if err != nil {
                logger.Printf("Error loading motions for page %d: %v\n", page, err)
                return