Switch to context API
This commit is contained in:
parent
6fe515ea52
commit
e0be1a6aa5
3 changed files with 124 additions and 63 deletions
|
@ -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}
|
||||
|
|
164
boardvoting.go
164
boardvoting.go
|
@ -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 = ¶ms
|
||||
if voter, ok := r.Context().Value(ctxVoter).(*Voter); ok {
|
||||
templateContext.Voter = voter
|
||||
}
|
||||
templateContext.Params = ¶ms
|
||||
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 = ¶ms
|
||||
voter, ok := getVoterFromRequest(r)
|
||||
if ok {
|
||||
templateContext.Voter = voter
|
||||
}
|
||||
templateContext.Params = ¶ms
|
||||
if params.ShowVotes {
|
||||
if err := decision.LoadVotes(); err != nil {
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
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()
|
||||
|
||||
|
|
19
models.go
19
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
|
||||
|
|
Loading…
Reference in a new issue