/* 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 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) }