You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
297 lines
6.5 KiB
Go
297 lines
6.5 KiB
Go
/*
|
|
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 (
|
|
"bytes"
|
|
"crypto/x509"
|
|
"encoding/pem"
|
|
"errors"
|
|
"fmt"
|
|
"html/template"
|
|
"io/fs"
|
|
"net/http"
|
|
"path/filepath"
|
|
"runtime/debug"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/Masterminds/sprig/v3"
|
|
"github.com/go-playground/form/v4"
|
|
"github.com/julienschmidt/httprouter"
|
|
"github.com/justinas/nosurf"
|
|
|
|
"git.cacert.org/cacert-boardvoting/internal/models"
|
|
"git.cacert.org/cacert-boardvoting/ui"
|
|
)
|
|
|
|
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)
|
|
}
|
|
|
|
func (app *application) decodePostForm(r *http.Request, dst any) error {
|
|
err := r.ParseForm()
|
|
if err != nil {
|
|
return fmt.Errorf("could not parse HTML form: %w", err)
|
|
}
|
|
|
|
err = app.formDecoder.Decode(dst, r.PostForm)
|
|
if err != nil {
|
|
var invalidDecoderError *form.InvalidDecoderError
|
|
|
|
if errors.As(err, &invalidDecoderError) {
|
|
panic(err)
|
|
}
|
|
|
|
return fmt.Errorf("could not decode form: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func newTemplateCache() (map[string]*template.Template, error) {
|
|
cache := map[string]*template.Template{}
|
|
|
|
pages, err := fs.Glob(ui.Files, "html/pages/*.html")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("could not find page templates: %w", err)
|
|
}
|
|
|
|
funcMaps := sprig.FuncMap()
|
|
funcMaps["nl2br"] = func(text string) template.HTML {
|
|
// #nosec G203 input is sanitized
|
|
return template.HTML(strings.ReplaceAll(template.HTMLEscapeString(text), "\n", "<br>"))
|
|
}
|
|
funcMaps["canManageUsers"] = func(v *models.User) (bool, error) {
|
|
return checkRole(v, models.RoleSecretary, models.RoleAdmin)
|
|
}
|
|
funcMaps["canVote"] = func(v *models.User) (bool, error) {
|
|
return checkRole(v, models.RoleVoter)
|
|
}
|
|
funcMaps["canStartVote"] = func(v *models.User) (bool, error) {
|
|
return checkRole(v, models.RoleVoter)
|
|
}
|
|
|
|
for _, page := range pages {
|
|
name := filepath.Base(page)
|
|
|
|
ts, err := template.New("").Funcs(funcMaps).ParseFS(
|
|
ui.Files,
|
|
"html/base.html",
|
|
"html/partials/*.html",
|
|
page,
|
|
)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("could not parse base template: %w", err)
|
|
}
|
|
|
|
cache[name] = ts
|
|
}
|
|
|
|
return cache, nil
|
|
}
|
|
|
|
type templateData struct {
|
|
PrevPage string
|
|
NextPage string
|
|
Motion *models.Motion
|
|
Motions []*models.Motion
|
|
User *models.User
|
|
Users []*models.User
|
|
Request *http.Request
|
|
Flashes []FlashMessage
|
|
Form any
|
|
ActiveNav topLevelNavItem
|
|
ActiveSubNav subLevelNavItem
|
|
CSRFToken string
|
|
}
|
|
|
|
func (app *application) newTemplateData(
|
|
r *http.Request,
|
|
nav topLevelNavItem,
|
|
subNav subLevelNavItem,
|
|
) *templateData {
|
|
user, _ := app.GetUser(r)
|
|
|
|
return &templateData{
|
|
Request: r,
|
|
User: user,
|
|
ActiveNav: nav,
|
|
ActiveSubNav: subNav,
|
|
Flashes: app.flashes(r),
|
|
CSRFToken: nosurf.Token(r),
|
|
}
|
|
}
|
|
|
|
func (app *application) render(w http.ResponseWriter, status int, page string, data *templateData) {
|
|
ts, ok := app.templateCache[page]
|
|
if !ok {
|
|
app.serverError(w, fmt.Errorf("the template %s does not exist", page))
|
|
|
|
return
|
|
}
|
|
|
|
buf := new(bytes.Buffer)
|
|
|
|
err := ts.ExecuteTemplate(buf, "base", data)
|
|
if err != nil {
|
|
app.serverError(w, err)
|
|
|
|
return
|
|
}
|
|
|
|
w.WriteHeader(status)
|
|
|
|
_, _ = buf.WriteTo(w)
|
|
}
|
|
|
|
func (app *application) motionFromRequestParam(
|
|
w http.ResponseWriter,
|
|
r *http.Request,
|
|
params httprouter.Params,
|
|
) *models.Motion {
|
|
tag := params.ByName("tag")
|
|
|
|
withVotes := r.URL.Query().Has("showvotes")
|
|
|
|
motion, err := app.motions.ByTag(r.Context(), tag, withVotes)
|
|
if err != nil {
|
|
app.serverError(w, err)
|
|
|
|
return nil
|
|
}
|
|
|
|
if motion.ID == 0 {
|
|
app.notFound(w)
|
|
|
|
return nil
|
|
}
|
|
|
|
return motion
|
|
}
|
|
|
|
func (app *application) userFromRequestParam(
|
|
w http.ResponseWriter, r *http.Request, params httprouter.Params, options ...models.UserListOption,
|
|
) *models.User {
|
|
userID, err := strconv.Atoi(params.ByName("id"))
|
|
if err != nil {
|
|
app.clientError(w, http.StatusBadRequest)
|
|
|
|
return nil
|
|
}
|
|
|
|
user, err := app.users.ByID(r.Context(), int64(userID), options...)
|
|
if err != nil {
|
|
app.serverError(w, err)
|
|
|
|
return nil
|
|
}
|
|
|
|
if user == nil {
|
|
app.notFound(w)
|
|
|
|
return nil
|
|
}
|
|
|
|
return user
|
|
}
|
|
|
|
func (app *application) choiceFromRequestParam(w http.ResponseWriter, params httprouter.Params) *models.VoteChoice {
|
|
choiceParam := params.ByName("choice")
|
|
|
|
choice, err := models.VoteChoiceFromString(choiceParam)
|
|
if err != nil {
|
|
app.clientError(w, http.StatusBadRequest)
|
|
|
|
return nil
|
|
}
|
|
|
|
return choice
|
|
}
|
|
|
|
func getPEMClientCert(r *http.Request) (string, error) {
|
|
cert := r.Context().Value(ctxAuthenticatedCert)
|
|
|
|
authenticatedCertificate, ok := cert.(*x509.Certificate)
|
|
if !ok {
|
|
return "", errors.New("could not handle certificate as x509.Certificate")
|
|
}
|
|
|
|
clientCertPEM := bytes.NewBuffer(make([]byte, 0))
|
|
|
|
err := pem.Encode(clientCertPEM, &pem.Block{Type: "CERTIFICATE", Bytes: authenticatedCertificate.Raw})
|
|
if err != nil {
|
|
return "", fmt.Errorf("error encoding client certificate: %w", err)
|
|
}
|
|
|
|
return clientCertPEM.String(), nil
|
|
}
|
|
|
|
type FlashVariant string
|
|
|
|
const (
|
|
flashWarning FlashVariant = "warning"
|
|
flashInfo FlashVariant = "info"
|
|
flashSuccess FlashVariant = "success"
|
|
flashError FlashVariant = "error"
|
|
)
|
|
|
|
type FlashMessage struct {
|
|
Variant FlashVariant
|
|
Title string
|
|
Message string
|
|
}
|
|
|
|
func (app *application) addFlash(r *http.Request, message *FlashMessage) {
|
|
flashes := app.flashes(r)
|
|
|
|
flashes = append(flashes, *message)
|
|
|
|
app.sessionManager.Put(
|
|
r.Context(),
|
|
"flashes",
|
|
flashes,
|
|
)
|
|
}
|
|
|
|
func (app *application) flashes(r *http.Request) []FlashMessage {
|
|
flashInstance := app.sessionManager.Pop(r.Context(), "flashes")
|
|
|
|
if flashInstance != nil {
|
|
flashes, ok := flashInstance.([]FlashMessage)
|
|
|
|
if ok {
|
|
return flashes
|
|
}
|
|
}
|
|
|
|
return make([]FlashMessage, 0)
|
|
}
|