/* 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 ( "encoding/json" "fmt" "net/http" "strings" "time" "github.com/julienschmidt/httprouter" "git.cacert.org/cacert-boardvoting/internal/forms" "git.cacert.org/cacert-boardvoting/internal/models" ) func checkRole(v *models.User, roles []string) (bool, error) { hasRole, err := v.HasRole(roles) if err != nil { return false, fmt.Errorf("could not determine user roles: %w", err) } return hasRole, nil } type topLevelNavItem string type subLevelNavItem string const ( topLevelNavMotions topLevelNavItem = "motions" topLevelNavUsers topLevelNavItem = "users" subLevelNavMotionsAll subLevelNavItem = "all-motions" subLevelNavMotionsUnvoted subLevelNavItem = "unvoted-motions" subLevelNavUsers subLevelNavItem = "users" ) func (m *templateData) motionPaginationOptions(limit int, first, last *time.Time) error { motions := m.Motions if len(motions) == limit && first.Before(motions[len(motions)-1].Proposed) { marshalled, err := motions[len(motions)-1].Proposed.MarshalText() if err != nil { return fmt.Errorf("could not serialize timestamp: %w", err) } m.NextPage = string(marshalled) } if len(motions) > 0 && last.After(motions[0].Proposed) { marshalled, err := motions[0].Proposed.MarshalText() if err != nil { return fmt.Errorf("could not serialize timestamp: %w", err) } m.PrevPage = string(marshalled) } return nil } func (app *application) motionList(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/motions/" { app.notFound(w) return } var ( listOptions *models.MotionListOptions err error ) listOptions, err = app.calculateMotionListOptions(r) if err != nil { app.clientError(w, http.StatusBadRequest) return } ctx := r.Context() motions, err := app.motions.GetMotions(ctx, listOptions) if err != nil { app.serverError(w, err) return } first, last, err := app.motions.TimestampRange(ctx, listOptions) if err != nil { app.serverError(w, err) return } templateData := app.newTemplateData(r, "motions", "all-motions") if listOptions.UnvotedOnly { templateData.ActiveSubNav = subLevelNavMotionsUnvoted } templateData.Motions = motions err = templateData.motionPaginationOptions(listOptions.Limit, first, last) if err != nil { app.serverError(w, err) return } app.render(w, http.StatusOK, "motions.html", templateData) } func (app *application) calculateMotionListOptions(r *http.Request) (*models.MotionListOptions, error) { const ( queryParamBefore = "before" queryParamAfter = "after" queryParamUnvoted = "unvoted" motionsPerPage = 10 ) listOptions := &models.MotionListOptions{Limit: motionsPerPage} if r.URL.Query().Has(queryParamAfter) { var after time.Time err := after.UnmarshalText([]byte(r.URL.Query().Get(queryParamAfter))) if err != nil { return nil, fmt.Errorf("could not unmarshal timestamp: %w", err) } listOptions.After = &after } else if r.URL.Query().Has(queryParamBefore) { var before time.Time err := before.UnmarshalText([]byte(r.URL.Query().Get(queryParamBefore))) if err != nil { return nil, fmt.Errorf("could not unmarshal timestamp: %w", err) } listOptions.Before = &before } if r.URL.Query().Has(queryParamUnvoted) { listOptions.UnvotedOnly = true voter, err := app.GetUser(r) if err != nil { return nil, fmt.Errorf("could not get voter: %w", err) } if voter != nil { listOptions.VoterID = voter.ID } } return listOptions, nil } func (app *application) motionDetails(w http.ResponseWriter, r *http.Request) { params := httprouter.ParamsFromContext(r.Context()) showVotes := r.URL.Query().Has("showvotes") motion := app.motionFromRequestParam(w, r, params, showVotes) if motion == nil { return } data := app.newTemplateData(r, topLevelNavMotions, subLevelNavMotionsAll) data.Motion = motion app.render(w, http.StatusOK, "motion.html", data) } func (app *application) newMotionForm(w http.ResponseWriter, r *http.Request) { data := app.newTemplateData(r, topLevelNavMotions, subLevelNavMotionsAll) data.Form = &forms.EditMotionForm{ Type: models.VoteTypeMotion, } app.render(w, http.StatusOK, "create_motion.html", data) } const hoursInDay = 24 func (app *application) newMotionSubmit(w http.ResponseWriter, r *http.Request) { var form forms.EditMotionForm err := app.decodePostForm(r, &form) if err != nil { app.clientError(w, http.StatusBadRequest) return } form.Validate() if !form.Valid() { data := app.newTemplateData(r, topLevelNavMotions, subLevelNavMotionsAll) data.Form = form app.render(w, http.StatusUnprocessableEntity, "create_motion.html", data) return } user, err := app.GetUser(r) if err != nil { app.clientError(w, http.StatusUnauthorized) return } now := time.Now().UTC() dueDuration := time.Duration(form.Due) * hoursInDay * time.Hour decisionID, err := app.motions.Create( r.Context(), user, form.Type, form.Title, form.Content, now, now.Add(dueDuration), ) if err != nil { app.serverError(w, err) return } decision, err := app.motions.GetByID(r.Context(), decisionID) if err != nil { app.serverError(w, err) return } app.mailNotifier.notifyChannel <- &NewDecisionNotification{ Decision: decision, Proposer: user, } app.sessionManager.Put(r.Context(), "flash", fmt.Sprintf("Started new motion %s: %s", decision.Tag, decision.Title)) http.Redirect(w, r, "/motions/", http.StatusSeeOther) } func (app *application) editMotionForm(w http.ResponseWriter, r *http.Request) { params := httprouter.ParamsFromContext(r.Context()) motion := app.motionFromRequestParam(w, r, params, false) if motion == nil { return } data := app.newTemplateData(r, topLevelNavMotions, subLevelNavMotionsAll) data.Motion = motion data.Form = &forms.EditMotionForm{ Title: motion.Title, Content: motion.Content, Type: motion.Type, } app.render(w, http.StatusOK, "edit_motion.html", data) } func (app *application) editMotionSubmit(w http.ResponseWriter, r *http.Request) { params := httprouter.ParamsFromContext(r.Context()) motion := app.motionFromRequestParam(w, r, params, false) if motion == nil { return } var form forms.EditMotionForm err := app.decodePostForm(r, &form) if err != nil { app.clientError(w, http.StatusBadRequest) return } form.Validate() if !form.Valid() { data := app.newTemplateData(r, topLevelNavMotions, subLevelNavMotionsAll) data.Form = form app.render(w, http.StatusUnprocessableEntity, "edit_motion.html", data) return } form.Normalize() user, err := app.GetUser(r) if err != nil { app.clientError(w, http.StatusUnauthorized) return } now := time.Now().UTC() dueDuration := time.Duration(form.Due) * hoursInDay * time.Hour err = app.motions.Update( r.Context(), motion.ID, func(m *models.Motion) { m.Type = form.Type m.Title = strings.TrimSpace(form.Title) m.Content = strings.TrimSpace(form.Content) m.Due = now.Add(dueDuration) }, ) if err != nil { app.serverError(w, err) return } decision, err := app.motions.GetByID(r.Context(), motion.ID) if err != nil { app.serverError(w, err) return } app.mailNotifier.notifyChannel <- &UpdateDecisionNotification{ Decision: decision, User: user, } app.sessionManager.Put( r.Context(), "flash", fmt.Sprintf("The motion %s has been modified!", decision.Tag), ) http.Redirect(w, r, "/motions/", http.StatusSeeOther) } func (app *application) withdrawMotionForm(_ http.ResponseWriter, _ *http.Request) { panic("not implemented") } func (app *application) withdrawMotionSubmit(_ http.ResponseWriter, _ *http.Request) { panic("not implemented") } func (app *application) voteForm(w http.ResponseWriter, r *http.Request) { params := httprouter.ParamsFromContext(r.Context()) motion := app.motionFromRequestParam(w, r, params, false) if motion == nil { return } choice := app.choiceFromRequestParam(w, params) if choice == nil { return } data := app.newTemplateData(r, topLevelNavMotions, subLevelNavMotionsAll) data.Motion = motion data.Form = &forms.DirectVoteForm{ Choice: choice, } app.render(w, http.StatusOK, "direct_vote.html", data) } func (app *application) voteSubmit(w http.ResponseWriter, r *http.Request) { params := httprouter.ParamsFromContext(r.Context()) motion := app.motionFromRequestParam(w, r, params, false) if motion == nil { return } choice := app.choiceFromRequestParam(w, params) if choice == nil { return } user, err := app.GetUser(r) if err != nil { app.clientError(w, http.StatusUnauthorized) return } clientCert, err := getPEMClientCert(r) if err != nil { app.serverError(w, err) return } if err := app.motions.UpdateVote(r.Context(), user.ID, motion.ID, func(v *models.Vote) { v.Vote = choice v.Voted = time.Now().UTC() v.Notes = fmt.Sprintf("Direct Vote\n\n%s", clientCert) }); err != nil { app.serverError(w, err) return } app.mailNotifier.notifyChannel <- &DirectVoteNotification{ Decision: motion, User: user, Choice: choice, } app.sessionManager.Put( r.Context(), "flash", fmt.Sprintf("Your vote for motion %s has been registered.", motion.Tag), ) http.Redirect(w, r, "/motions/", http.StatusSeeOther) } func (app *application) proxyVoteForm(w http.ResponseWriter, r *http.Request) { params := httprouter.ParamsFromContext(r.Context()) motion := app.motionFromRequestParam(w, r, params, false) if motion == nil { return } data := app.newTemplateData(r, topLevelNavMotions, subLevelNavMotionsAll) data.Motion = motion potentialVoters, err := app.users.PotentialVoters(r.Context()) if err != nil { app.serverError(w, err) return } data.Form = &forms.ProxyVoteForm{ Voters: potentialVoters, } app.render(w, http.StatusOK, "proxy_vote.html", data) } func (app *application) proxyVoteSubmit(w http.ResponseWriter, r *http.Request) { params := httprouter.ParamsFromContext(r.Context()) motion := app.motionFromRequestParam(w, r, params, false) if motion == nil { return } var form forms.ProxyVoteForm err := app.decodePostForm(r, &form) if err != nil { app.clientError(w, http.StatusBadRequest) return } form.Validate() if !form.Valid() { data := app.newTemplateData(r, topLevelNavMotions, subLevelNavMotionsAll) data.Motion = motion potentialVoters, err := app.users.PotentialVoters(r.Context()) if err != nil { app.serverError(w, err) return } form.Voters = potentialVoters data.Form = form app.render(w, http.StatusUnprocessableEntity, "proxy_vote.html", data) return } form.Normalize() user, err := app.GetUser(r) if err != nil { app.clientError(w, http.StatusUnauthorized) return } clientCert, err := getPEMClientCert(r) if err != nil { app.serverError(w, err) return } voter, err := app.users.LoadVoter(r.Context(), form.VoterID) if err != nil { app.serverError(w, err) return } if err := app.motions.UpdateVote(r.Context(), form.VoterID, motion.ID, func(v *models.Vote) { v.Vote = form.Choice v.Voted = time.Now().UTC() v.Notes = fmt.Sprintf("Proxy-Vote by %s\n\n%s\n\n%s", user.Name, form.Justification, clientCert) }); err != nil { app.serverError(w, err) return } app.mailNotifier.notifyChannel <- &ProxyVoteNotification{ Decision: motion, User: user, Voter: voter, Choice: form.Choice, Justification: form.Justification, } app.sessionManager.Put( r.Context(), "flash", fmt.Sprintf("Your proxy vote for %s for motion %s has been registered.", voter.Name, motion.Tag), ) http.Redirect(w, r, "/motions/", http.StatusSeeOther) } func (app *application) userList(w http.ResponseWriter, r *http.Request) { data := app.newTemplateData(r, topLevelNavUsers, subLevelNavUsers) app.render(w, http.StatusOK, "users.html", data) } func (app *application) submitUserRoles(_ http.ResponseWriter, _ *http.Request) { panic("not implemented") } func (app *application) editUserForm(_ http.ResponseWriter, _ *http.Request) { panic("not implemented") } func (app *application) editUserSubmit(_ http.ResponseWriter, _ *http.Request) { panic("not implemented") } func (app *application) userAddEmailForm(_ http.ResponseWriter, _ *http.Request) { panic("not implemented") } func (app *application) userAddEmailSubmit(_ http.ResponseWriter, _ *http.Request) { panic("not implemented") } func (app *application) deleteUserForm(_ http.ResponseWriter, _ *http.Request) { panic("not implemented") } func (app *application) deleteUserSubmit(_ http.ResponseWriter, _ *http.Request) { panic("not implemented") } func (app *application) healthCheck(w http.ResponseWriter, _ *http.Request) { response := struct { DB string `json:"database"` }{DB: "ok"} enc := json.NewEncoder(w) w.Header().Set("Content-Type", "application/json") err := app.motions.DB.Ping() if err != nil { w.WriteHeader(http.StatusInternalServerError) response.DB = "FAILED" } _ = enc.Encode(response) }