2022-10-15 17:58:58 +00:00
|
|
|
/*
|
2024-06-08 08:29:35 +00:00
|
|
|
Copyright CAcert Inc.
|
2022-10-15 17:58:58 +00:00
|
|
|
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 handlers
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
|
|
|
"encoding/json"
|
|
|
|
"errors"
|
|
|
|
"fmt"
|
|
|
|
"net/http"
|
|
|
|
"strconv"
|
|
|
|
"strings"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
"github.com/go-chi/chi/v5"
|
|
|
|
"github.com/go-playground/form/v4"
|
|
|
|
"github.com/justinas/nosurf"
|
|
|
|
|
|
|
|
"git.cacert.org/cacert-boardvoting/internal/forms"
|
|
|
|
"git.cacert.org/cacert-boardvoting/internal/jobs"
|
|
|
|
"git.cacert.org/cacert-boardvoting/internal/models"
|
|
|
|
"git.cacert.org/cacert-boardvoting/internal/notifications"
|
|
|
|
)
|
|
|
|
|
|
|
|
func ClientError(w http.ResponseWriter, status int) {
|
|
|
|
http.Error(w, http.StatusText(status), status)
|
|
|
|
}
|
|
|
|
|
|
|
|
func NotFound(w http.ResponseWriter) {
|
|
|
|
ClientError(w, http.StatusNotFound)
|
|
|
|
}
|
|
|
|
|
|
|
|
type topLevelNavItem string
|
|
|
|
type subLevelNavItem string
|
|
|
|
|
|
|
|
const (
|
|
|
|
topLevelNavMotions topLevelNavItem = "motions"
|
|
|
|
topLevelNavUsers topLevelNavItem = "users"
|
|
|
|
|
|
|
|
subLevelNavMotionsAll subLevelNavItem = "all-motions"
|
|
|
|
subLevelNavMotionsUnvoted subLevelNavItem = "unvoted-motions"
|
|
|
|
subLevelNavUsers subLevelNavItem = "users"
|
|
|
|
subLevelNavVoters subLevelNavItem = "manage-voters"
|
|
|
|
)
|
|
|
|
|
|
|
|
const hoursInDay = 24
|
|
|
|
|
|
|
|
func choiceFromRequestParam(w http.ResponseWriter, r *http.Request) *models.VoteChoice {
|
|
|
|
choice, err := models.VoteChoiceFromString(chi.URLParam(r, "choice"))
|
|
|
|
if err != nil {
|
|
|
|
ClientError(w, http.StatusBadRequest)
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
return choice
|
|
|
|
}
|
|
|
|
|
|
|
|
type UserAwareHandler interface {
|
|
|
|
UserModel() *models.UserModel
|
|
|
|
}
|
|
|
|
|
|
|
|
func getVoter(ctx context.Context, w http.ResponseWriter, u UserAwareHandler, voterID int64) *models.User {
|
|
|
|
users := u.UserModel()
|
|
|
|
|
|
|
|
voter, err := users.ByID(ctx, voterID, users.WithRoles())
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
var isVoter bool
|
|
|
|
|
|
|
|
if isVoter, err = voter.HasRole(models.RoleVoter); err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
if !isVoter {
|
|
|
|
ClientError(w, http.StatusBadRequest)
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
return voter
|
|
|
|
}
|
|
|
|
|
|
|
|
func userFromRequestParam(
|
|
|
|
ctx context.Context,
|
|
|
|
w http.ResponseWriter, r *http.Request, u UserAwareHandler, options ...models.UserListOption,
|
|
|
|
) *models.User {
|
|
|
|
userID, err := strconv.Atoi(chi.URLParam(r, "id"))
|
|
|
|
if err != nil {
|
|
|
|
ClientError(w, http.StatusBadRequest)
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
user, err := u.UserModel().ByID(ctx, int64(userID), options...)
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
if user == nil {
|
|
|
|
NotFound(w)
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
return user
|
|
|
|
}
|
|
|
|
|
|
|
|
type MotionAwareHandler interface {
|
|
|
|
MotionModel() *models.MotionModel
|
|
|
|
}
|
|
|
|
|
|
|
|
func motionFromRequestParam(
|
|
|
|
ctx context.Context,
|
|
|
|
w http.ResponseWriter,
|
|
|
|
r *http.Request,
|
|
|
|
m MotionAwareHandler,
|
|
|
|
) *models.Motion {
|
|
|
|
withVotes := r.URL.Query().Has("showvotes")
|
|
|
|
|
|
|
|
motion, err := m.MotionModel().ByTag(ctx, chi.URLParam(r, "tag"), withVotes)
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
if motion.ID == 0 {
|
|
|
|
NotFound(w)
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
return motion
|
|
|
|
}
|
|
|
|
|
|
|
|
func newTemplateData(
|
|
|
|
ctx context.Context,
|
|
|
|
r *http.Request,
|
|
|
|
flash *FlashHandler,
|
|
|
|
nav topLevelNavItem,
|
|
|
|
subNav subLevelNavItem,
|
|
|
|
) *TemplateData {
|
|
|
|
user, _ := getUser(r)
|
|
|
|
|
|
|
|
return &TemplateData{
|
|
|
|
Request: r,
|
|
|
|
User: user,
|
|
|
|
ActiveNav: nav,
|
|
|
|
ActiveSubNav: subNav,
|
|
|
|
Flashes: flash.flashes(ctx),
|
|
|
|
CSRFToken: nosurf.Token(r),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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) {
|
|
|
|
marshaled, err := motions[len(motions)-1].Proposed.MarshalText()
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("could not serialize timestamp: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
m.NextPage = string(marshaled)
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(motions) > 0 && last.After(motions[0].Proposed) {
|
|
|
|
marshaled, err := motions[0].Proposed.MarshalText()
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("could not serialize timestamp: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
m.PrevPage = string(marshaled)
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func 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 := 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 emailFromRequestParam(r *http.Request, user *models.User) (string, error) {
|
|
|
|
emailAddresses, err := user.EmailAddresses()
|
|
|
|
if err != nil {
|
|
|
|
return "", fmt.Errorf("could not get email addresses: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
emailParam := chi.URLParam(r, "address")
|
|
|
|
|
|
|
|
for _, address := range emailAddresses {
|
|
|
|
if emailParam == address {
|
|
|
|
return emailParam, nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return "", nil
|
|
|
|
}
|
|
|
|
|
|
|
|
type CommonParams struct {
|
|
|
|
Flashes *FlashHandler
|
|
|
|
TemplateCache *TemplateCache
|
|
|
|
FormDecoder *form.Decoder
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *CommonParams) decodePostForm(r *http.Request, dst forms.Form) error {
|
|
|
|
err := r.ParseForm()
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("could not parse HTML form: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
err = c.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
|
|
|
|
}
|
|
|
|
|
|
|
|
type MotionHandler struct {
|
|
|
|
motions *models.MotionModel
|
|
|
|
mailNotifier *notifications.MailNotifier
|
|
|
|
jobScheduler *jobs.JobScheduler
|
|
|
|
CommonParams
|
|
|
|
}
|
|
|
|
|
|
|
|
func (m *MotionHandler) newTemplateData(ctx context.Context, r *http.Request, subNav subLevelNavItem) *TemplateData {
|
|
|
|
return newTemplateData(ctx, r, m.Flashes, topLevelNavMotions, subNav)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (m *MotionHandler) MotionModel() *models.MotionModel {
|
|
|
|
return m.motions
|
|
|
|
}
|
|
|
|
|
|
|
|
func (m *MotionHandler) List(w http.ResponseWriter, r *http.Request) {
|
|
|
|
var (
|
|
|
|
listOptions *models.MotionListOptions
|
|
|
|
err error
|
|
|
|
)
|
|
|
|
|
|
|
|
listOptions, err = calculateMotionListOptions(r)
|
|
|
|
if err != nil {
|
|
|
|
ClientError(w, http.StatusBadRequest)
|
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
ctx := r.Context()
|
|
|
|
|
|
|
|
motions, err := m.motions.List(ctx, listOptions)
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
first, last, err := m.motions.TimestampRange(ctx, listOptions)
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
templateData := m.newTemplateData(r.Context(), r, subLevelNavMotionsAll)
|
|
|
|
|
|
|
|
if listOptions.UnvotedOnly {
|
|
|
|
templateData.ActiveSubNav = subLevelNavMotionsUnvoted
|
|
|
|
}
|
|
|
|
|
|
|
|
templateData.Motions = motions
|
|
|
|
|
|
|
|
err = templateData.motionPaginationOptions(listOptions.Limit, first, last)
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
m.TemplateCache.render(w, http.StatusOK, "motions.html", templateData)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (m *MotionHandler) Details(w http.ResponseWriter, r *http.Request) {
|
|
|
|
motion := motionFromRequestParam(r.Context(), w, r, m)
|
|
|
|
if motion == nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
data := m.newTemplateData(r.Context(), r, subLevelNavMotionsAll)
|
|
|
|
|
|
|
|
data.Motion = motion
|
|
|
|
|
|
|
|
m.TemplateCache.render(w, http.StatusOK, "motion.html", data)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (m *MotionHandler) NewForm(w http.ResponseWriter, r *http.Request) {
|
|
|
|
data := m.newTemplateData(r.Context(), r, subLevelNavMotionsAll)
|
|
|
|
data.Form = &forms.EditMotionForm{
|
|
|
|
Type: models.VoteTypeMotion,
|
|
|
|
}
|
|
|
|
|
|
|
|
m.TemplateCache.render(w, http.StatusOK, "create_motion.html", data)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (m *MotionHandler) New(w http.ResponseWriter, r *http.Request) {
|
|
|
|
var motionForm forms.EditMotionForm
|
|
|
|
|
|
|
|
err := m.decodePostForm(r, &motionForm)
|
|
|
|
if err != nil {
|
|
|
|
ClientError(w, http.StatusBadRequest)
|
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := motionForm.Validate(); err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
if !motionForm.Valid() {
|
|
|
|
data := m.newTemplateData(r.Context(), r, subLevelNavMotionsAll)
|
|
|
|
data.Form = &motionForm
|
|
|
|
|
|
|
|
m.TemplateCache.render(w, http.StatusUnprocessableEntity, "create_motion.html", data)
|
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
motionForm.Normalize()
|
|
|
|
|
|
|
|
user, err := getUser(r)
|
|
|
|
if err != nil {
|
|
|
|
ClientError(w, http.StatusUnauthorized)
|
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
now := time.Now().UTC()
|
|
|
|
dueDuration := time.Duration(motionForm.Due) * hoursInDay * time.Hour
|
|
|
|
|
|
|
|
decisionID, err := m.motions.Create(
|
|
|
|
r.Context(),
|
|
|
|
user,
|
|
|
|
motionForm.Type,
|
|
|
|
motionForm.Title,
|
|
|
|
motionForm.Content,
|
|
|
|
now,
|
|
|
|
now.Add(dueDuration),
|
|
|
|
)
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
decision, err := m.motions.ByID(r.Context(), decisionID)
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
m.mailNotifier.Notify(¬ifications.NewDecisionNotification{
|
|
|
|
Decision: decision,
|
|
|
|
Proposer: user,
|
|
|
|
})
|
|
|
|
|
|
|
|
m.jobScheduler.Reschedule(jobs.JobIDCloseDecisions, jobs.JobIDRemindVoters)
|
|
|
|
|
|
|
|
m.Flashes.addFlash(r.Context(), &FlashMessage{
|
|
|
|
Variant: flashSuccess,
|
|
|
|
Title: "New motion started",
|
|
|
|
Message: fmt.Sprintf("Started new motion %s: %s", decision.Tag, decision.Title),
|
|
|
|
})
|
|
|
|
|
|
|
|
http.Redirect(w, r, "/motions/", http.StatusSeeOther)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (m *MotionHandler) EditForm(w http.ResponseWriter, r *http.Request) {
|
|
|
|
motion := motionFromRequestParam(r.Context(), w, r, m)
|
|
|
|
if motion == nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
data := m.newTemplateData(r.Context(), r, subLevelNavMotionsAll)
|
|
|
|
|
|
|
|
data.Motion = motion
|
|
|
|
|
|
|
|
data.Form = &forms.EditMotionForm{
|
|
|
|
Title: motion.Title,
|
|
|
|
Content: motion.Content,
|
|
|
|
Type: motion.Type,
|
|
|
|
}
|
|
|
|
|
|
|
|
m.TemplateCache.render(w, http.StatusOK, "edit_motion.html", data)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (m *MotionHandler) Edit(w http.ResponseWriter, r *http.Request) {
|
|
|
|
motion := motionFromRequestParam(r.Context(), w, r, m)
|
|
|
|
if motion == nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
var motionForm forms.EditMotionForm
|
|
|
|
|
|
|
|
err := m.decodePostForm(r, &motionForm)
|
|
|
|
if err != nil {
|
|
|
|
ClientError(w, http.StatusBadRequest)
|
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := motionForm.Validate(); err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
if !motionForm.Valid() {
|
|
|
|
data := m.newTemplateData(r.Context(), r, subLevelNavMotionsAll)
|
|
|
|
data.Form = &motionForm
|
|
|
|
|
|
|
|
m.TemplateCache.render(w, http.StatusUnprocessableEntity, "edit_motion.html", data)
|
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
motionForm.Normalize()
|
|
|
|
|
|
|
|
user, err := getUser(r)
|
|
|
|
if err != nil {
|
|
|
|
ClientError(w, http.StatusUnauthorized)
|
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
now := time.Now().UTC()
|
|
|
|
dueDuration := time.Duration(motionForm.Due) * hoursInDay * time.Hour
|
|
|
|
|
|
|
|
err = m.motions.Update(
|
|
|
|
r.Context(),
|
|
|
|
motion.ID,
|
|
|
|
func(m *models.Motion) {
|
|
|
|
m.Type = motionForm.Type
|
|
|
|
m.Title = strings.TrimSpace(motionForm.Title)
|
|
|
|
m.Content = strings.TrimSpace(motionForm.Content)
|
|
|
|
m.Due = now.Add(dueDuration)
|
|
|
|
},
|
|
|
|
)
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
decision, err := m.motions.ByID(r.Context(), motion.ID)
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
m.mailNotifier.Notify(¬ifications.UpdateDecisionNotification{
|
|
|
|
Decision: decision,
|
|
|
|
User: user,
|
|
|
|
})
|
|
|
|
|
|
|
|
m.jobScheduler.Reschedule(jobs.JobIDCloseDecisions, jobs.JobIDRemindVoters)
|
|
|
|
|
|
|
|
m.Flashes.addFlash(r.Context(), &FlashMessage{
|
|
|
|
Variant: flashInfo,
|
|
|
|
Title: "Motion modified",
|
|
|
|
Message: fmt.Sprintf("The motion %s has been modified!", decision.Tag),
|
|
|
|
})
|
|
|
|
|
|
|
|
http.Redirect(w, r, "/motions/", http.StatusSeeOther)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (m *MotionHandler) WithdrawForm(w http.ResponseWriter, r *http.Request) {
|
|
|
|
motion := motionFromRequestParam(r.Context(), w, r, m)
|
|
|
|
if motion == nil {
|
|
|
|
NotFound(w)
|
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
data := m.newTemplateData(r.Context(), r, subLevelNavMotionsAll)
|
|
|
|
|
|
|
|
data.Motion = motion
|
|
|
|
|
|
|
|
m.TemplateCache.render(w, http.StatusOK, "withdraw_motion.html", data)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (m *MotionHandler) Withdraw(w http.ResponseWriter, r *http.Request) {
|
|
|
|
motion := motionFromRequestParam(r.Context(), w, r, m)
|
|
|
|
if motion == nil {
|
|
|
|
NotFound(w)
|
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
user, err := getUser(r)
|
|
|
|
if err != nil {
|
|
|
|
ClientError(w, http.StatusUnauthorized)
|
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
err = m.motions.Withdraw(r.Context(), motion.ID)
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
m.mailNotifier.Notify(¬ifications.WithDrawMotionNotification{Motion: motion, Voter: user})
|
|
|
|
|
|
|
|
m.jobScheduler.Reschedule(jobs.JobIDCloseDecisions, jobs.JobIDRemindVoters)
|
|
|
|
|
|
|
|
m.Flashes.addFlash(r.Context(), &FlashMessage{
|
|
|
|
Variant: flashWarning,
|
|
|
|
Title: "Motion withdrawn",
|
|
|
|
Message: fmt.Sprintf("The motion %s has been withdrawn!", motion.Tag),
|
|
|
|
})
|
|
|
|
|
|
|
|
http.Redirect(w, r, "/motions/", http.StatusSeeOther)
|
|
|
|
}
|
|
|
|
|
|
|
|
func NewMotionHandler(
|
|
|
|
motions *models.MotionModel,
|
|
|
|
mailNotifier *notifications.MailNotifier,
|
|
|
|
jobScheduler *jobs.JobScheduler,
|
|
|
|
params CommonParams,
|
|
|
|
) *MotionHandler {
|
|
|
|
return &MotionHandler{
|
|
|
|
motions: motions,
|
|
|
|
mailNotifier: mailNotifier,
|
|
|
|
jobScheduler: jobScheduler,
|
|
|
|
CommonParams: params,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
type VoteHandler struct {
|
|
|
|
motions *models.MotionModel
|
|
|
|
users *models.UserModel
|
|
|
|
mailNotifier *notifications.MailNotifier
|
|
|
|
CommonParams
|
|
|
|
}
|
|
|
|
|
|
|
|
func (v *VoteHandler) newTemplateData(ctx context.Context, r *http.Request, subNav subLevelNavItem) *TemplateData {
|
|
|
|
return newTemplateData(ctx, r, v.Flashes, topLevelNavMotions, subNav)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (v *VoteHandler) UserModel() *models.UserModel {
|
|
|
|
return v.users
|
|
|
|
}
|
|
|
|
|
|
|
|
func (v *VoteHandler) MotionModel() *models.MotionModel {
|
|
|
|
return v.motions
|
|
|
|
}
|
|
|
|
|
|
|
|
func (v *VoteHandler) VoteForm(w http.ResponseWriter, r *http.Request) {
|
|
|
|
motion := motionFromRequestParam(r.Context(), w, r, v)
|
|
|
|
if motion == nil {
|
|
|
|
NotFound(w)
|
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
choice := choiceFromRequestParam(w, r)
|
|
|
|
if choice == nil {
|
|
|
|
NotFound(w)
|
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
data := v.newTemplateData(r.Context(), r, subLevelNavMotionsAll)
|
|
|
|
|
|
|
|
data.Motion = motion
|
|
|
|
|
|
|
|
data.Form = &forms.DirectVoteForm{
|
|
|
|
Choice: choice,
|
|
|
|
}
|
|
|
|
|
|
|
|
v.TemplateCache.render(w, http.StatusOK, "direct_vote.html", data)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (v *VoteHandler) Vote(w http.ResponseWriter, r *http.Request) {
|
|
|
|
motion := motionFromRequestParam(r.Context(), w, r, v)
|
|
|
|
if motion == nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
choice := choiceFromRequestParam(w, r)
|
|
|
|
if choice == nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
user, err := getUser(r)
|
|
|
|
if err != nil {
|
|
|
|
ClientError(w, http.StatusUnauthorized)
|
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
clientCert, err := getPEMClientCert(r)
|
|
|
|
if err != nil {
|
|
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
|
|
_, _ = fmt.Fprint(w, http.StatusText(http.StatusInternalServerError))
|
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := v.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 {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
v.mailNotifier.Notify(¬ifications.DirectVoteNotification{
|
|
|
|
Decision: motion, User: user, Choice: choice,
|
|
|
|
})
|
|
|
|
|
|
|
|
v.Flashes.addFlash(r.Context(), &FlashMessage{
|
|
|
|
Variant: flashSuccess,
|
|
|
|
Title: "Vote registered",
|
|
|
|
Message: fmt.Sprintf("Your vote for motion %s has been registered.", motion.Tag),
|
|
|
|
})
|
|
|
|
|
|
|
|
http.Redirect(w, r, "/motions/", http.StatusSeeOther)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (v *VoteHandler) ProxyVoteForm(w http.ResponseWriter, r *http.Request) {
|
|
|
|
motion := motionFromRequestParam(r.Context(), w, r, v)
|
|
|
|
if motion == nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
data := v.newTemplateData(r.Context(), r, subLevelNavMotionsAll)
|
|
|
|
|
|
|
|
data.Motion = motion
|
|
|
|
|
|
|
|
potentialVoters, err := v.users.Voters(r.Context())
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
data.Form = &forms.ProxyVoteForm{
|
|
|
|
Voters: potentialVoters,
|
|
|
|
}
|
|
|
|
|
|
|
|
v.TemplateCache.render(w, http.StatusOK, "proxy_vote.html", data)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (v *VoteHandler) ProxyVote(w http.ResponseWriter, r *http.Request) {
|
|
|
|
motion := motionFromRequestParam(r.Context(), w, r, v)
|
|
|
|
if motion == nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
var voteForm forms.ProxyVoteForm
|
|
|
|
|
|
|
|
err := v.decodePostForm(r, &voteForm)
|
|
|
|
if err != nil {
|
|
|
|
ClientError(w, http.StatusBadRequest)
|
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := voteForm.Validate(); err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
if !voteForm.Valid() {
|
|
|
|
data := v.newTemplateData(r.Context(), r, subLevelNavMotionsAll)
|
|
|
|
data.Motion = motion
|
|
|
|
|
|
|
|
potentialVoters, err := v.users.Voters(r.Context())
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
voteForm.Voters = potentialVoters
|
|
|
|
data.Form = &voteForm
|
|
|
|
|
|
|
|
v.TemplateCache.render(w, http.StatusUnprocessableEntity, "proxy_vote.html", data)
|
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
voteForm.Normalize()
|
|
|
|
|
|
|
|
user, err := getUser(r)
|
|
|
|
if err != nil {
|
|
|
|
ClientError(w, http.StatusUnauthorized)
|
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
clientCert, err := getPEMClientCert(r)
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
voter := getVoter(r.Context(), w, v, voteForm.Voter.ID)
|
|
|
|
if voter == nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := v.motions.UpdateVote(r.Context(), voteForm.Voter.ID, motion.ID, func(v *models.Vote) {
|
|
|
|
v.Vote = voteForm.Choice
|
|
|
|
v.Voted = time.Now().UTC()
|
|
|
|
v.Notes = fmt.Sprintf("Proxy-Vote by %s\n\n%s\n\n%s", user.Name, voteForm.Justification, clientCert)
|
|
|
|
}); err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
v.mailNotifier.Notify(¬ifications.ProxyVoteNotification{
|
|
|
|
Decision: motion, User: user, Voter: voter, Choice: voteForm.Choice, Justification: voteForm.Justification,
|
|
|
|
})
|
|
|
|
|
|
|
|
v.Flashes.addFlash(r.Context(), &FlashMessage{
|
|
|
|
Variant: flashSuccess,
|
|
|
|
Title: "Proxy vote registered",
|
|
|
|
Message: 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 NewVoteHandler(
|
|
|
|
motions *models.MotionModel,
|
|
|
|
users *models.UserModel,
|
|
|
|
mailNotifier *notifications.MailNotifier,
|
|
|
|
params CommonParams,
|
|
|
|
) *VoteHandler {
|
|
|
|
return &VoteHandler{motions: motions, users: users, mailNotifier: mailNotifier, CommonParams: params}
|
|
|
|
}
|
|
|
|
|
|
|
|
type UserHandler struct {
|
|
|
|
users *models.UserModel
|
|
|
|
CommonParams
|
|
|
|
}
|
|
|
|
|
|
|
|
func (u *UserHandler) UserModel() *models.UserModel {
|
|
|
|
return u.users
|
|
|
|
}
|
|
|
|
|
|
|
|
func (u *UserHandler) newTemplateData(ctx context.Context, r *http.Request, subNav subLevelNavItem) *TemplateData {
|
|
|
|
return newTemplateData(ctx, r, u.Flashes, topLevelNavUsers, subNav)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (u *UserHandler) deleteEmailParams(
|
|
|
|
ctx context.Context,
|
|
|
|
w http.ResponseWriter,
|
|
|
|
r *http.Request,
|
|
|
|
) (*models.User, string, error) {
|
|
|
|
userToEdit := userFromRequestParam(ctx, w, r, u, u.users.WithEmailAddresses())
|
|
|
|
if userToEdit == nil {
|
|
|
|
return nil, "", nil
|
|
|
|
}
|
|
|
|
|
|
|
|
emailAddress, err := emailFromRequestParam(r, userToEdit)
|
|
|
|
if err != nil {
|
|
|
|
return nil, "", err
|
|
|
|
}
|
|
|
|
|
|
|
|
return userToEdit, emailAddress, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (u *UserHandler) List(w http.ResponseWriter, r *http.Request) {
|
|
|
|
data := u.newTemplateData(r.Context(), r, subLevelNavUsers)
|
|
|
|
|
|
|
|
users, err := u.users.List(
|
|
|
|
r.Context(),
|
|
|
|
u.users.WithRoles(),
|
|
|
|
u.users.CanDelete(),
|
|
|
|
)
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
data.Users = users
|
|
|
|
|
|
|
|
u.TemplateCache.render(w, http.StatusOK, "users.html", data)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (u *UserHandler) CreateForm(w http.ResponseWriter, r *http.Request) {
|
|
|
|
data := u.newTemplateData(r.Context(), r, subLevelNavUsers)
|
|
|
|
|
|
|
|
data.Form = &forms.NewUserForm{}
|
|
|
|
|
|
|
|
u.TemplateCache.render(w, http.StatusOK, "create_user.html", data)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (u *UserHandler) Create(w http.ResponseWriter, r *http.Request) {
|
|
|
|
admin, err := getUser(r)
|
|
|
|
if err != nil {
|
|
|
|
ClientError(w, http.StatusUnauthorized)
|
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
var userForm forms.NewUserForm
|
|
|
|
|
|
|
|
if err := u.decodePostForm(r, &userForm); err != nil {
|
|
|
|
ClientError(w, http.StatusBadRequest)
|
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := userForm.Validate(); err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
if !userForm.Valid() {
|
|
|
|
data := u.newTemplateData(r.Context(), r, subLevelNavUsers)
|
|
|
|
|
|
|
|
data.Form = &userForm
|
|
|
|
|
|
|
|
u.TemplateCache.render(w, http.StatusUnprocessableEntity, "create_user.html", data)
|
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
userForm.Normalize()
|
|
|
|
|
|
|
|
createUserParams := &models.CreateUserParams{
|
|
|
|
Admin: admin,
|
|
|
|
Name: userForm.Name,
|
|
|
|
Reminder: userForm.EmailAddress,
|
|
|
|
Emails: []string{userForm.EmailAddress},
|
|
|
|
Reasoning: userForm.Reasoning,
|
|
|
|
}
|
|
|
|
|
|
|
|
userID, err := u.users.Create(r.Context(), createUserParams)
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
u.Flashes.addFlash(r.Context(), &FlashMessage{
|
|
|
|
Variant: flashSuccess,
|
|
|
|
Title: "User created",
|
|
|
|
Message: fmt.Sprintf("Created new user %s", userForm.Name),
|
|
|
|
})
|
|
|
|
|
|
|
|
http.Redirect(w, r, fmt.Sprintf("/users/%d", userID), http.StatusSeeOther)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (u *UserHandler) EditForm(w http.ResponseWriter, r *http.Request) {
|
|
|
|
userToEdit := userFromRequestParam(r.Context(), w, r, u, u.users.WithRoles(), u.users.WithEmailAddresses())
|
|
|
|
if userToEdit == nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
roles, err := userToEdit.Roles()
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
emailAddresses, err := userToEdit.EmailAddresses()
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
roleNames := make([]string, len(roles))
|
|
|
|
for i := range roles {
|
|
|
|
roleNames[i] = roles[i].Name
|
|
|
|
}
|
|
|
|
|
|
|
|
data := u.newTemplateData(r.Context(), r, subLevelNavUsers)
|
|
|
|
|
|
|
|
data.Form = &forms.EditUserForm{
|
|
|
|
User: userToEdit,
|
|
|
|
Name: userToEdit.Name,
|
|
|
|
MailAddresses: emailAddresses,
|
|
|
|
AllRoles: models.AllRoles,
|
|
|
|
ReminderMail: userToEdit.Reminder.String,
|
|
|
|
Roles: roleNames,
|
|
|
|
}
|
|
|
|
|
|
|
|
u.TemplateCache.render(w, http.StatusOK, "edit_user.html", data)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (u *UserHandler) Edit(w http.ResponseWriter, r *http.Request) {
|
|
|
|
userToEdit := userFromRequestParam(r.Context(), w, r, u, u.users.WithRoles(), u.users.WithEmailAddresses())
|
|
|
|
if userToEdit == nil {
|
|
|
|
NotFound(w)
|
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
var userForm forms.EditUserForm
|
|
|
|
|
|
|
|
if err := u.decodePostForm(r, &userForm); err != nil {
|
|
|
|
ClientError(w, http.StatusBadRequest)
|
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
userForm.User = userToEdit
|
|
|
|
userForm.AllRoles = models.AllRoles
|
|
|
|
|
|
|
|
if err := userForm.Validate(); err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
data := u.newTemplateData(r.Context(), r, subLevelNavUsers)
|
|
|
|
|
|
|
|
if !userForm.Valid() {
|
|
|
|
roles, err := userToEdit.Roles()
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
emailAddresses, err := userToEdit.EmailAddresses()
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
roleNames := make([]string, len(roles))
|
|
|
|
for i := range roles {
|
|
|
|
roleNames[i] = roles[i].Name
|
|
|
|
}
|
|
|
|
|
|
|
|
userForm.MailAddresses = emailAddresses
|
|
|
|
userForm.Roles = roleNames
|
|
|
|
|
|
|
|
data.Form = &userForm
|
|
|
|
|
|
|
|
u.TemplateCache.render(w, http.StatusUnprocessableEntity, "edit_user.html", data)
|
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
userForm.Normalize()
|
|
|
|
userForm.UpdateUser(userToEdit)
|
|
|
|
|
|
|
|
if err := u.users.EditUser(r.Context(), userToEdit, data.User, userForm.Roles, userForm.Reasoning); err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
u.Flashes.addFlash(r.Context(), &FlashMessage{
|
|
|
|
Variant: flashInfo,
|
|
|
|
Title: "User modified",
|
|
|
|
Message: fmt.Sprintf("User %s has been modified.", userToEdit.Name),
|
|
|
|
})
|
|
|
|
|
|
|
|
http.Redirect(w, r, "/users/", http.StatusSeeOther)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (u *UserHandler) AddEmailForm(w http.ResponseWriter, r *http.Request) {
|
|
|
|
userToEdit := userFromRequestParam(r.Context(), w, r, u, u.users.WithEmailAddresses())
|
|
|
|
if userToEdit == nil {
|
|
|
|
NotFound(w)
|
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
emailAddresses, err := userToEdit.EmailAddresses()
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
data := u.newTemplateData(r.Context(), r, subLevelNavUsers)
|
|
|
|
|
|
|
|
data.Form = &forms.AddEmailForm{
|
|
|
|
User: userToEdit,
|
|
|
|
EmailAddresses: emailAddresses,
|
|
|
|
}
|
|
|
|
|
|
|
|
u.TemplateCache.render(w, http.StatusOK, "add_email.html", data)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (u *UserHandler) AddEmail(w http.ResponseWriter, r *http.Request) {
|
|
|
|
userToEdit := userFromRequestParam(r.Context(), w, r, u, u.users.WithEmailAddresses())
|
|
|
|
if userToEdit == nil {
|
|
|
|
NotFound(w)
|
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
var emailForm forms.AddEmailForm
|
|
|
|
|
|
|
|
if err := u.decodePostForm(r, &emailForm); err != nil {
|
|
|
|
ClientError(w, http.StatusBadRequest)
|
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
emailForm.User = userToEdit
|
|
|
|
|
|
|
|
if err := emailForm.Validate(); err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
data := u.newTemplateData(r.Context(), r, subLevelNavUsers)
|
|
|
|
|
|
|
|
if emailForm.Valid() {
|
|
|
|
emailExists, err := u.users.EmailExists(r.Context(), emailForm.EmailAddress)
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
if emailExists {
|
|
|
|
emailForm.FieldErrors = map[string]string{
|
|
|
|
"email_address": "Email address must be unique",
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if !emailForm.Valid() {
|
|
|
|
emailAddresses, err := userToEdit.EmailAddresses()
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
emailForm.EmailAddresses = emailAddresses
|
|
|
|
|
|
|
|
data.Form = &emailForm
|
|
|
|
|
|
|
|
u.TemplateCache.render(w, http.StatusUnprocessableEntity, "add_email.html", data)
|
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
emailForm.Normalize()
|
|
|
|
|
|
|
|
if err := u.users.AddEmail(
|
|
|
|
r.Context(),
|
|
|
|
userToEdit,
|
|
|
|
data.User,
|
|
|
|
emailForm.EmailAddress,
|
|
|
|
emailForm.Reasoning,
|
|
|
|
); err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
u.Flashes.addFlash(r.Context(), &FlashMessage{
|
|
|
|
Variant: flashSuccess,
|
|
|
|
Title: "Email address added",
|
|
|
|
Message: fmt.Sprintf(
|
|
|
|
"Added email address %s for user %s",
|
|
|
|
emailForm.EmailAddress,
|
|
|
|
userToEdit.Name,
|
|
|
|
),
|
|
|
|
})
|
|
|
|
|
|
|
|
http.Redirect(w, r, fmt.Sprintf("/users/%d/", userToEdit.ID), http.StatusSeeOther)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (u *UserHandler) DeleteEmailForm(w http.ResponseWriter, r *http.Request) {
|
|
|
|
userToEdit, emailAddress, err := u.deleteEmailParams(r.Context(), w, r)
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
if userToEdit == nil || emailAddress == "" {
|
|
|
|
NotFound(w)
|
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if userToEdit.Reminder.String == emailAddress {
|
|
|
|
// delete of reminder address should not happen
|
|
|
|
ClientError(w, http.StatusBadRequest)
|
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
data := u.newTemplateData(r.Context(), r, subLevelNavUsers)
|
|
|
|
|
|
|
|
data.Form = &forms.DeleteEmailForm{
|
|
|
|
User: userToEdit,
|
|
|
|
EmailAddress: emailAddress,
|
|
|
|
}
|
|
|
|
|
|
|
|
u.TemplateCache.render(w, http.StatusOK, "delete_email.html", data)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (u *UserHandler) DeleteEmail(w http.ResponseWriter, r *http.Request) {
|
|
|
|
userToEdit, emailAddress, err := u.deleteEmailParams(r.Context(), w, r)
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
if userToEdit == nil || emailAddress == "" {
|
|
|
|
NotFound(w)
|
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if userToEdit.Reminder.String == emailAddress {
|
|
|
|
// delete of reminder address should not happen
|
|
|
|
ClientError(w, http.StatusBadRequest)
|
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
admin, err := getUser(r)
|
|
|
|
if err != nil {
|
|
|
|
ClientError(w, http.StatusUnauthorized)
|
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
var deleteEmailForm forms.DeleteEmailForm
|
|
|
|
|
|
|
|
if err := u.decodePostForm(r, &deleteEmailForm); err != nil {
|
|
|
|
ClientError(w, http.StatusBadRequest)
|
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := deleteEmailForm.Validate(); err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
if !deleteEmailForm.Valid() {
|
|
|
|
deleteEmailForm.EmailAddress = emailAddress
|
|
|
|
deleteEmailForm.User = userToEdit
|
|
|
|
|
|
|
|
data := u.newTemplateData(r.Context(), r, subLevelNavUsers)
|
|
|
|
|
|
|
|
data.Form = &deleteEmailForm
|
|
|
|
|
|
|
|
u.TemplateCache.render(w, http.StatusUnprocessableEntity, "delete_email.html", data)
|
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
deleteEmailForm.Normalize()
|
|
|
|
|
|
|
|
if err := u.users.DeleteEmail(r.Context(), userToEdit, admin, emailAddress, deleteEmailForm.Reasoning); err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
u.Flashes.addFlash(r.Context(), &FlashMessage{
|
|
|
|
Variant: flashWarning,
|
|
|
|
Title: "Email address deleted",
|
|
|
|
Message: fmt.Sprintf(
|
|
|
|
"Deleted email address %s of user %s",
|
|
|
|
deleteEmailForm.EmailAddress,
|
|
|
|
userToEdit.Name,
|
|
|
|
),
|
|
|
|
})
|
|
|
|
|
|
|
|
http.Redirect(w, r, fmt.Sprintf("/users/%d/", userToEdit.ID), http.StatusSeeOther)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (u *UserHandler) DeleteForm(w http.ResponseWriter, r *http.Request) {
|
|
|
|
userToDelete := userFromRequestParam(r.Context(), w, r, u, u.users.CanDelete())
|
|
|
|
if userToDelete == nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if !userToDelete.CanDelete() {
|
|
|
|
NotFound(w)
|
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
data := u.newTemplateData(r.Context(), r, subLevelNavUsers)
|
|
|
|
|
|
|
|
data.Form = &forms.DeleteUserForm{
|
|
|
|
User: userToDelete,
|
|
|
|
}
|
|
|
|
|
|
|
|
u.TemplateCache.render(w, http.StatusOK, "delete_user.html", data)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (u *UserHandler) Delete(w http.ResponseWriter, r *http.Request) {
|
|
|
|
userToDelete := userFromRequestParam(r.Context(), w, r, u, u.users.CanDelete())
|
|
|
|
if userToDelete == nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if !userToDelete.CanDelete() {
|
|
|
|
NotFound(w)
|
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
var deleteUserForm forms.DeleteUserForm
|
|
|
|
|
|
|
|
err := u.decodePostForm(r, &deleteUserForm)
|
|
|
|
if err != nil {
|
|
|
|
ClientError(w, http.StatusBadRequest)
|
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
deleteUserForm.User = userToDelete
|
|
|
|
|
|
|
|
if err := deleteUserForm.Validate(); err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
data := u.newTemplateData(r.Context(), r, subLevelNavUsers)
|
|
|
|
|
|
|
|
if !deleteUserForm.Valid() {
|
|
|
|
data.Form = &deleteUserForm
|
|
|
|
|
|
|
|
u.TemplateCache.render(w, http.StatusUnprocessableEntity, "delete_user.html", data)
|
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
deleteUserForm.Normalize()
|
|
|
|
|
|
|
|
if err = u.users.DeleteUser(r.Context(), userToDelete, data.User, deleteUserForm.Reasoning); err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
u.Flashes.addFlash(r.Context(), &FlashMessage{
|
|
|
|
Variant: flashWarning,
|
|
|
|
Title: "User deleted",
|
|
|
|
Message: fmt.Sprintf("User %s has been deleted.", userToDelete.Name),
|
|
|
|
})
|
|
|
|
|
|
|
|
http.Redirect(w, r, "/users/", http.StatusSeeOther)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (u *UserHandler) ChangeVotersForm(w http.ResponseWriter, r *http.Request) {
|
|
|
|
data := u.newTemplateData(r.Context(), r, subLevelNavVoters)
|
|
|
|
|
|
|
|
allUsers, err := u.users.List(r.Context(), u.users.WithRoles())
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
voterIDs := make([]int64, 0)
|
|
|
|
|
|
|
|
for _, user := range allUsers {
|
|
|
|
isVoter, err := user.HasRole(models.RoleVoter)
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
if isVoter {
|
|
|
|
voterIDs = append(voterIDs, user.ID)
|
|
|
|
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
data.Form = &forms.ChooseVoterForm{
|
|
|
|
Users: allUsers,
|
|
|
|
VoterIDs: voterIDs,
|
|
|
|
}
|
|
|
|
|
|
|
|
u.TemplateCache.render(w, http.StatusOK, "choose_voters.html", data)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (u *UserHandler) ChangeVoters(w http.ResponseWriter, r *http.Request) {
|
|
|
|
admin, err := getUser(r)
|
|
|
|
if err != nil {
|
|
|
|
ClientError(w, http.StatusUnauthorized)
|
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
var voterForm forms.ChooseVoterForm
|
|
|
|
|
|
|
|
if err = u.decodePostForm(r, &voterForm); err != nil {
|
|
|
|
ClientError(w, http.StatusBadRequest)
|
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if err = voterForm.Validate(); err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
if !voterForm.Valid() {
|
|
|
|
data := u.newTemplateData(r.Context(), r, subLevelNavVoters)
|
|
|
|
|
|
|
|
allUsers, err := u.users.List(r.Context(), u.users.WithRoles())
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
voterForm.Users = allUsers
|
|
|
|
|
|
|
|
data.Form = &voterForm
|
|
|
|
|
|
|
|
u.TemplateCache.render(w, http.StatusUnprocessableEntity, "choose_voters.html", data)
|
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
voterForm.Normalize()
|
|
|
|
|
|
|
|
if err = u.users.ChooseVoters(r.Context(), admin, voterForm.VoterIDs, voterForm.Reasoning); err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
u.Flashes.addFlash(r.Context(), &FlashMessage{
|
|
|
|
Variant: flashSuccess,
|
|
|
|
Title: "Voters selected",
|
|
|
|
Message: "A new list of voters has been selected",
|
|
|
|
})
|
|
|
|
|
|
|
|
http.Redirect(w, r, "/users/", http.StatusSeeOther)
|
|
|
|
}
|
|
|
|
|
|
|
|
func NewUserHandler(
|
|
|
|
users *models.UserModel,
|
|
|
|
params CommonParams,
|
|
|
|
) *UserHandler {
|
|
|
|
return &UserHandler{users: users, CommonParams: params}
|
|
|
|
}
|
|
|
|
|
|
|
|
type HealthCheck struct {
|
|
|
|
mailNotifier *notifications.MailNotifier
|
|
|
|
motions *models.MotionModel
|
|
|
|
}
|
|
|
|
|
|
|
|
func NewHealthCheck(notifier *notifications.MailNotifier, motions *models.MotionModel) *HealthCheck {
|
|
|
|
return &HealthCheck{mailNotifier: notifier, motions: motions}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (h *HealthCheck) ServeHTTP(w http.ResponseWriter, _ *http.Request) {
|
|
|
|
const (
|
|
|
|
ok = "OK"
|
|
|
|
failed = "FAILED"
|
|
|
|
)
|
|
|
|
|
|
|
|
response := struct {
|
|
|
|
DB string `json:"database"`
|
|
|
|
Mail string `json:"mail"`
|
|
|
|
}{DB: ok, Mail: ok}
|
|
|
|
|
|
|
|
enc := json.NewEncoder(w)
|
|
|
|
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
|
|
w.Header().Set("Refresh", "10")
|
|
|
|
w.Header().Add("Cache-Control", "no-store")
|
|
|
|
|
|
|
|
var (
|
|
|
|
err error
|
|
|
|
hasErrors = false
|
|
|
|
)
|
|
|
|
|
|
|
|
if err = h.mailNotifier.Ping(); err != nil {
|
|
|
|
hasErrors = true
|
|
|
|
|
|
|
|
response.Mail = failed
|
|
|
|
}
|
|
|
|
|
|
|
|
if err = h.motions.DB.Ping(); err != nil {
|
|
|
|
hasErrors = true
|
|
|
|
|
|
|
|
response.DB = failed
|
|
|
|
}
|
|
|
|
|
|
|
|
if hasErrors {
|
|
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
|
|
} else {
|
|
|
|
w.WriteHeader(http.StatusOK)
|
|
|
|
}
|
|
|
|
|
|
|
|
_ = enc.Encode(response)
|
|
|
|
}
|