cacert-boardvoting/cmd/boardvoting/handlers.go

1145 lines
25 KiB
Go
Raw Normal View History

2022-05-09 19:09:24 +00:00
/*
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"
2022-05-09 19:09:24 +00:00
"net/http"
2022-05-27 18:45:04 +00:00
"strings"
"time"
2022-05-27 15:39:54 +00:00
"git.cacert.org/cacert-boardvoting/internal/forms"
"git.cacert.org/cacert-boardvoting/internal/models"
)
func checkRole(v *models.User, roles ...models.RoleName) (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"
subLevelNavVoters subLevelNavItem = "manage-voters"
)
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) {
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.List(ctx, listOptions)
if err != nil {
panic(err)
}
first, last, err := app.motions.TimestampRange(ctx, listOptions)
if err != nil {
panic(err)
}
2022-09-26 09:58:36 +00:00
templateData := app.newTemplateData(r.Context(), r, "motions", "all-motions")
if listOptions.UnvotedOnly {
templateData.ActiveSubNav = subLevelNavMotionsUnvoted
}
templateData.Motions = motions
2022-05-09 19:09:24 +00:00
err = templateData.motionPaginationOptions(listOptions.Limit, first, last)
2022-05-09 19:09:24 +00:00
if err != nil {
panic(err)
2022-05-09 19:09:24 +00:00
}
2022-05-22 10:19:25 +00:00
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
2022-05-09 19:09:24 +00:00
}
func (app *application) motionDetails(w http.ResponseWriter, r *http.Request) {
2022-09-26 09:58:36 +00:00
motion := app.motionFromRequestParam(r.Context(), w, r)
if motion == nil {
2022-05-09 19:09:24 +00:00
return
}
2022-09-26 09:58:36 +00:00
data := app.newTemplateData(r.Context(), r, topLevelNavMotions, subLevelNavMotionsAll)
data.Motion = motion
app.render(w, http.StatusOK, "motion.html", data)
}
func (app *application) newMotionForm(w http.ResponseWriter, r *http.Request) {
2022-09-26 09:58:36 +00:00
data := app.newTemplateData(r.Context(), r, topLevelNavMotions, subLevelNavMotionsAll)
2022-05-27 18:45:04 +00:00
data.Form = &forms.EditMotionForm{
Type: models.VoteTypeMotion,
}
app.render(w, http.StatusOK, "create_motion.html", data)
}
2022-05-26 19:04:47 +00:00
const hoursInDay = 24
func (app *application) newMotionSubmit(w http.ResponseWriter, r *http.Request) {
2022-05-27 18:45:04 +00:00
var form forms.EditMotionForm
err := app.decodePostForm(r, &form)
if err != nil {
app.clientError(w, http.StatusBadRequest)
return
}
if err := form.Validate(); err != nil {
panic(err)
}
if !form.Valid() {
2022-09-26 09:58:36 +00:00
data := app.newTemplateData(r.Context(), r, topLevelNavMotions, subLevelNavMotionsAll)
data.Form = &form
app.render(w, http.StatusUnprocessableEntity, "create_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
decisionID, err := app.motions.Create(
r.Context(),
user,
form.Type,
form.Title,
form.Content,
now,
now.Add(dueDuration),
)
if err != nil {
panic(err)
}
decision, err := app.motions.ByID(r.Context(), decisionID)
if err != nil {
panic(err)
}
app.mailNotifier.Notify(&NewDecisionNotification{
2022-05-27 15:39:54 +00:00
Decision: decision,
Proposer: user,
})
app.jobScheduler.Reschedule(JobIDCloseDecisions, JobIDRemindVoters)
2022-09-26 09:58:36 +00:00
app.addFlash(r.Context(), &FlashMessage{
2022-06-04 11:52:53 +00:00
Variant: flashSuccess,
Title: "New motion started",
Message: fmt.Sprintf("Started new motion %s: %s", decision.Tag, decision.Title),
})
2022-05-26 19:04:47 +00:00
http.Redirect(w, r, "/motions/", http.StatusSeeOther)
}
2022-05-26 19:04:47 +00:00
func (app *application) editMotionForm(w http.ResponseWriter, r *http.Request) {
2022-09-26 09:58:36 +00:00
motion := app.motionFromRequestParam(r.Context(), w, r)
2022-05-27 15:39:54 +00:00
if motion == nil {
2022-05-26 19:04:47 +00:00
return
}
2022-09-26 09:58:36 +00:00
data := app.newTemplateData(r.Context(), r, topLevelNavMotions, subLevelNavMotionsAll)
2022-05-26 19:04:47 +00:00
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)
}
2022-05-26 19:04:47 +00:00
func (app *application) editMotionSubmit(w http.ResponseWriter, r *http.Request) {
2022-09-26 09:58:36 +00:00
motion := app.motionFromRequestParam(r.Context(), w, r)
2022-05-27 15:39:54 +00:00
if motion == nil {
2022-05-26 19:04:47 +00:00
return
}
var form forms.EditMotionForm
2022-05-27 15:39:54 +00:00
err := app.decodePostForm(r, &form)
2022-05-26 19:04:47 +00:00
if err != nil {
app.clientError(w, http.StatusBadRequest)
return
}
if err := form.Validate(); err != nil {
panic(err)
}
2022-05-26 19:04:47 +00:00
if !form.Valid() {
2022-09-26 09:58:36 +00:00
data := app.newTemplateData(r.Context(), r, topLevelNavMotions, subLevelNavMotionsAll)
data.Form = &form
2022-05-26 19:04:47 +00:00
app.render(w, http.StatusUnprocessableEntity, "edit_motion.html", data)
return
}
2022-05-27 18:45:04 +00:00
form.Normalize()
2022-05-26 19:04:47 +00:00
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,
2022-05-27 18:45:04 +00:00
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)
},
2022-05-26 19:04:47 +00:00
)
2022-05-27 15:39:54 +00:00
if err != nil {
panic(err)
2022-05-27 15:39:54 +00:00
}
2022-05-26 19:04:47 +00:00
decision, err := app.motions.ByID(r.Context(), motion.ID)
2022-05-26 19:04:47 +00:00
if err != nil {
panic(err)
2022-05-26 19:04:47 +00:00
}
app.mailNotifier.Notify(&UpdateDecisionNotification{
2022-05-27 15:39:54 +00:00
Decision: decision,
User: user,
})
app.jobScheduler.Reschedule(JobIDCloseDecisions, JobIDRemindVoters)
2022-05-26 19:04:47 +00:00
2022-09-26 09:58:36 +00:00
app.addFlash(r.Context(), &FlashMessage{
2022-06-04 11:52:53 +00:00
Variant: flashInfo,
Title: "Motion modified",
Message: fmt.Sprintf("The motion %s has been modified!", decision.Tag),
})
2022-05-26 19:04:47 +00:00
http.Redirect(w, r, "/motions/", http.StatusSeeOther)
}
2022-05-29 10:01:58 +00:00
func (app *application) withdrawMotionForm(w http.ResponseWriter, r *http.Request) {
2022-09-26 09:58:36 +00:00
motion := app.motionFromRequestParam(r.Context(), w, r)
2022-05-29 10:01:58 +00:00
if motion == nil {
app.notFound(w)
return
}
2022-09-26 09:58:36 +00:00
data := app.newTemplateData(r.Context(), r, topLevelNavMotions, subLevelNavMotionsAll)
2022-05-29 10:01:58 +00:00
data.Motion = motion
app.render(w, http.StatusOK, "withdraw_motion.html", data)
}
2022-05-29 10:01:58 +00:00
func (app *application) withdrawMotionSubmit(w http.ResponseWriter, r *http.Request) {
2022-09-26 09:58:36 +00:00
motion := app.motionFromRequestParam(r.Context(), w, r)
2022-05-29 10:01:58 +00:00
if motion == nil {
app.notFound(w)
return
}
user, err := app.GetUser(r)
if err != nil {
app.clientError(w, http.StatusUnauthorized)
return
}
err = app.motions.Withdraw(r.Context(), motion.ID)
2022-05-29 10:01:58 +00:00
if err != nil {
panic(err)
2022-05-29 10:01:58 +00:00
}
app.mailNotifier.Notify(&WithDrawMotionNotification{motion, user})
app.jobScheduler.Reschedule(JobIDCloseDecisions, JobIDRemindVoters)
2022-05-29 10:01:58 +00:00
2022-09-26 09:58:36 +00:00
app.addFlash(r.Context(), &FlashMessage{
2022-06-04 11:52:53 +00:00
Variant: flashWarning,
Title: "Motion withdrawn",
Message: fmt.Sprintf("The motion %s has been withdrawn!", motion.Tag),
})
2022-05-29 10:01:58 +00:00
http.Redirect(w, r, "/motions/", http.StatusSeeOther)
}
2022-05-27 15:39:54 +00:00
func (app *application) voteForm(w http.ResponseWriter, r *http.Request) {
2022-09-26 09:58:36 +00:00
motion := app.motionFromRequestParam(r.Context(), w, r)
2022-05-27 15:39:54 +00:00
if motion == nil {
2022-05-29 10:01:58 +00:00
app.notFound(w)
2022-05-27 15:39:54 +00:00
return
}
choice := app.choiceFromRequestParam(w, r)
2022-05-27 15:39:54 +00:00
if choice == nil {
2022-05-29 10:01:58 +00:00
app.notFound(w)
2022-05-27 15:39:54 +00:00
return
}
2022-09-26 09:58:36 +00:00
data := app.newTemplateData(r.Context(), r, topLevelNavMotions, subLevelNavMotionsAll)
2022-05-27 15:39:54 +00:00
data.Motion = motion
data.Form = &forms.DirectVoteForm{
Choice: choice,
}
app.render(w, http.StatusOK, "direct_vote.html", data)
}
2022-05-27 15:39:54 +00:00
func (app *application) voteSubmit(w http.ResponseWriter, r *http.Request) {
2022-09-26 09:58:36 +00:00
motion := app.motionFromRequestParam(r.Context(), w, r)
2022-05-27 15:39:54 +00:00
if motion == nil {
return
}
choice := app.choiceFromRequestParam(w, r)
2022-05-27 15:39:54 +00:00
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 {
panic(err)
2022-05-27 15:39:54 +00:00
}
2022-05-27 18:45:04 +00:00
if err := app.motions.UpdateVote(r.Context(), user.ID, motion.ID, func(v *models.Vote) {
2022-05-27 15:39:54 +00:00
v.Vote = choice
v.Voted = time.Now().UTC()
v.Notes = fmt.Sprintf("Direct Vote\n\n%s", clientCert)
2022-05-27 18:45:04 +00:00
}); err != nil {
panic(err)
2022-05-27 15:39:54 +00:00
}
app.mailNotifier.Notify(&DirectVoteNotification{
2022-05-27 15:39:54 +00:00
Decision: motion, User: user, Choice: choice,
})
2022-05-27 15:39:54 +00:00
2022-09-26 09:58:36 +00:00
app.addFlash(r.Context(), &FlashMessage{
2022-06-04 11:52:53 +00:00
Variant: flashSuccess,
Title: "Vote registered",
Message: fmt.Sprintf("Your vote for motion %s has been registered.", motion.Tag),
})
2022-05-27 15:39:54 +00:00
2022-05-27 18:45:04 +00:00
http.Redirect(w, r, "/motions/", http.StatusSeeOther)
}
2022-05-27 18:45:04 +00:00
func (app *application) proxyVoteForm(w http.ResponseWriter, r *http.Request) {
2022-09-26 09:58:36 +00:00
motion := app.motionFromRequestParam(r.Context(), w, r)
2022-05-27 18:45:04 +00:00
if motion == nil {
return
}
2022-09-26 09:58:36 +00:00
data := app.newTemplateData(r.Context(), r, topLevelNavMotions, subLevelNavMotionsAll)
2022-05-27 18:45:04 +00:00
data.Motion = motion
potentialVoters, err := app.users.Voters(r.Context())
2022-05-27 18:45:04 +00:00
if err != nil {
panic(err)
2022-05-27 18:45:04 +00:00
}
data.Form = &forms.ProxyVoteForm{
Voters: potentialVoters,
}
app.render(w, http.StatusOK, "proxy_vote.html", data)
}
2022-05-27 18:45:04 +00:00
func (app *application) proxyVoteSubmit(w http.ResponseWriter, r *http.Request) {
2022-09-26 09:58:36 +00:00
motion := app.motionFromRequestParam(r.Context(), w, r)
2022-05-27 18:45:04 +00:00
if motion == nil {
return
}
var form forms.ProxyVoteForm
err := app.decodePostForm(r, &form)
if err != nil {
app.clientError(w, http.StatusBadRequest)
return
}
if err := form.Validate(); err != nil {
panic(err)
}
2022-05-27 18:45:04 +00:00
if !form.Valid() {
2022-09-26 09:58:36 +00:00
data := app.newTemplateData(r.Context(), r, topLevelNavMotions, subLevelNavMotionsAll)
2022-05-27 18:45:04 +00:00
data.Motion = motion
potentialVoters, err := app.users.Voters(r.Context())
2022-05-27 18:45:04 +00:00
if err != nil {
panic(err)
2022-05-27 18:45:04 +00:00
}
form.Voters = potentialVoters
data.Form = &form
2022-05-27 18:45:04 +00:00
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 {
panic(err)
2022-05-27 18:45:04 +00:00
}
2022-09-26 09:58:36 +00:00
voter := app.getVoter(r.Context(), w, form.Voter.ID)
if voter == nil {
2022-05-27 18:45:04 +00:00
return
}
if err := app.motions.UpdateVote(r.Context(), form.Voter.ID, motion.ID, func(v *models.Vote) {
2022-05-27 18:45:04 +00:00
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 {
panic(err)
2022-05-27 18:45:04 +00:00
}
app.mailNotifier.Notify(&ProxyVoteNotification{
2022-05-27 18:45:04 +00:00
Decision: motion, User: user, Voter: voter, Choice: form.Choice, Justification: form.Justification,
})
2022-05-27 18:45:04 +00:00
2022-09-26 09:58:36 +00:00
app.addFlash(r.Context(), &FlashMessage{
2022-06-04 11:52:53 +00:00
Variant: flashSuccess,
Title: "Proxy vote registered",
Message: fmt.Sprintf(
"Your proxy vote for %s for motion %s has been registered.",
voter.Name,
motion.Tag,
),
})
2022-05-27 18:45:04 +00:00
http.Redirect(w, r, "/motions/", http.StatusSeeOther)
}
func (app *application) userList(w http.ResponseWriter, r *http.Request) {
2022-09-26 09:58:36 +00:00
data := app.newTemplateData(r.Context(), r, topLevelNavUsers, subLevelNavUsers)
2022-05-29 18:46:52 +00:00
users, err := app.users.List(
r.Context(),
2022-06-02 21:14:38 +00:00
app.users.WithRoles(),
app.users.CanDelete(),
2022-05-29 18:46:52 +00:00
)
if err != nil {
panic(err)
2022-05-29 18:46:52 +00:00
}
data.Users = users
app.render(w, http.StatusOK, "users.html", data)
}
2022-06-02 21:14:38 +00:00
func (app *application) editUserForm(w http.ResponseWriter, r *http.Request) {
2022-09-26 09:58:36 +00:00
userToEdit := app.userFromRequestParam(r.Context(), w, r, app.users.WithRoles(), app.users.WithEmailAddresses())
2022-06-02 21:14:38 +00:00
if userToEdit == nil {
return
}
roles, err := userToEdit.Roles()
if err != nil {
panic(err)
2022-06-02 21:14:38 +00:00
}
emailAddresses, err := userToEdit.EmailAddresses()
if err != nil {
panic(err)
2022-06-02 21:14:38 +00:00
}
roleNames := make([]string, len(roles))
for i := range roles {
roleNames[i] = roles[i].Name
}
2022-09-26 09:58:36 +00:00
data := app.newTemplateData(r.Context(), r, topLevelNavUsers, subLevelNavUsers)
2022-06-02 21:14:38 +00:00
data.Form = &forms.EditUserForm{
User: userToEdit,
Name: userToEdit.Name,
MailAddresses: emailAddresses,
AllRoles: models.AllRoles,
ReminderMail: userToEdit.Reminder.String,
Roles: roleNames,
}
app.render(w, http.StatusOK, "edit_user.html", data)
}
2022-06-02 21:14:38 +00:00
func (app *application) editUserSubmit(w http.ResponseWriter, r *http.Request) {
2022-09-26 09:58:36 +00:00
userToEdit := app.userFromRequestParam(r.Context(), w, r, app.users.WithRoles(), app.users.WithEmailAddresses())
2022-06-02 21:14:38 +00:00
if userToEdit == nil {
2022-06-03 18:57:20 +00:00
app.notFound(w)
2022-06-02 21:14:38 +00:00
return
}
var form forms.EditUserForm
2022-06-03 18:57:20 +00:00
if err := app.decodePostForm(r, &form); err != nil {
2022-06-02 21:14:38 +00:00
app.clientError(w, http.StatusBadRequest)
return
}
form.User = userToEdit
form.AllRoles = models.AllRoles
if err := form.Validate(); err != nil {
panic(err)
2022-06-02 21:14:38 +00:00
}
2022-09-26 09:58:36 +00:00
data := app.newTemplateData(r.Context(), r, topLevelNavUsers, subLevelNavUsers)
2022-06-02 21:14:38 +00:00
if !form.Valid() {
roles, err := userToEdit.Roles()
if err != nil {
panic(err)
2022-06-02 21:14:38 +00:00
}
emailAddresses, err := userToEdit.EmailAddresses()
if err != nil {
panic(err)
2022-06-02 21:14:38 +00:00
}
roleNames := make([]string, len(roles))
for i := range roles {
roleNames[i] = roles[i].Name
}
form.MailAddresses = emailAddresses
form.Roles = roleNames
data.Form = &form
app.render(w, http.StatusUnprocessableEntity, "edit_user.html", data)
return
}
form.Normalize()
form.UpdateUser(userToEdit)
2022-06-03 18:57:20 +00:00
if err := app.users.EditUser(r.Context(), userToEdit, data.User, form.Roles, form.Reasoning); err != nil {
panic(err)
2022-06-02 21:14:38 +00:00
}
2022-09-26 09:58:36 +00:00
app.addFlash(r.Context(), &FlashMessage{
2022-06-04 11:52:53 +00:00
Variant: flashInfo,
Title: "User modified",
Message: fmt.Sprintf("User %s has been modified.", userToEdit.Name),
})
2022-06-02 21:14:38 +00:00
http.Redirect(w, r, "/users/", http.StatusSeeOther)
}
2022-06-03 18:57:20 +00:00
func (app *application) userAddEmailForm(w http.ResponseWriter, r *http.Request) {
2022-09-26 09:58:36 +00:00
userToEdit := app.userFromRequestParam(r.Context(), w, r, app.users.WithEmailAddresses())
2022-06-03 18:57:20 +00:00
if userToEdit == nil {
app.notFound(w)
return
}
emailAddresses, err := userToEdit.EmailAddresses()
if err != nil {
panic(err)
2022-06-03 18:57:20 +00:00
}
2022-09-26 09:58:36 +00:00
data := app.newTemplateData(r.Context(), r, topLevelNavUsers, subLevelNavUsers)
2022-06-03 18:57:20 +00:00
data.Form = &forms.AddEmailForm{
User: userToEdit,
EmailAddresses: emailAddresses,
}
app.render(w, http.StatusOK, "add_email.html", data)
}
2022-06-03 18:57:20 +00:00
func (app *application) userAddEmailSubmit(w http.ResponseWriter, r *http.Request) {
2022-09-26 09:58:36 +00:00
userToEdit := app.userFromRequestParam(r.Context(), w, r, app.users.WithEmailAddresses())
2022-06-03 18:57:20 +00:00
if userToEdit == nil {
app.notFound(w)
return
}
var form forms.AddEmailForm
if err := app.decodePostForm(r, &form); err != nil {
app.clientError(w, http.StatusBadRequest)
return
}
form.User = userToEdit
if err := form.Validate(); err != nil {
panic(err)
}
2022-06-03 18:57:20 +00:00
2022-09-26 09:58:36 +00:00
data := app.newTemplateData(r.Context(), r, topLevelNavUsers, subLevelNavUsers)
2022-06-03 18:57:20 +00:00
if form.Valid() {
emailExists, err := app.users.EmailExists(r.Context(), form.EmailAddress)
if err != nil {
panic(err)
2022-06-03 18:57:20 +00:00
}
if emailExists {
form.FieldErrors = map[string]string{
"email_address": "Email address must be unique",
}
}
}
if !form.Valid() {
emailAddresses, err := userToEdit.EmailAddresses()
if err != nil {
panic(err)
2022-06-03 18:57:20 +00:00
}
form.EmailAddresses = emailAddresses
data.Form = &form
app.render(w, http.StatusUnprocessableEntity, "add_email.html", data)
return
}
form.Normalize()
if err := app.users.AddEmail(r.Context(), userToEdit, data.User, form.EmailAddress, form.Reasoning); err != nil {
panic(err)
2022-06-03 18:57:20 +00:00
}
2022-09-26 09:58:36 +00:00
app.addFlash(r.Context(), &FlashMessage{
2022-06-04 11:52:53 +00:00
Variant: flashSuccess,
Title: "Email address added",
Message: fmt.Sprintf(
"Added email address %s for user %s",
form.EmailAddress,
userToEdit.Name,
),
})
2022-06-03 18:57:20 +00:00
http.Redirect(w, r, fmt.Sprintf("/users/%d/", userToEdit.ID), http.StatusSeeOther)
}
2022-06-04 11:53:07 +00:00
func (app *application) userDeleteEmailForm(w http.ResponseWriter, r *http.Request) {
2022-09-26 09:58:36 +00:00
userToEdit, emailAddress, err := app.deleteEmailParams(r.Context(), w, r)
2022-06-04 11:53:07 +00:00
if err != nil {
panic(err)
2022-06-04 11:53:07 +00:00
}
if userToEdit == nil || emailAddress == "" {
app.notFound(w)
return
}
if userToEdit.Reminder.String == emailAddress {
// delete of reminder address should not happen
app.clientError(w, http.StatusBadRequest)
return
}
2022-09-26 09:58:36 +00:00
data := app.newTemplateData(r.Context(), r, topLevelNavUsers, subLevelNavUsers)
2022-06-04 11:53:07 +00:00
data.Form = &forms.DeleteEmailForm{
User: userToEdit,
EmailAddress: emailAddress,
}
app.render(w, http.StatusOK, "delete_email.html", data)
2022-06-02 21:14:38 +00:00
}
2022-06-04 11:53:07 +00:00
func (app *application) userDeleteEmailSubmit(w http.ResponseWriter, r *http.Request) {
2022-09-26 09:58:36 +00:00
userToEdit, emailAddress, err := app.deleteEmailParams(r.Context(), w, r)
2022-06-04 11:53:07 +00:00
if err != nil {
panic(err)
2022-06-04 11:53:07 +00:00
}
if userToEdit == nil || emailAddress == "" {
app.notFound(w)
return
}
if userToEdit.Reminder.String == emailAddress {
// delete of reminder address should not happen
app.clientError(w, http.StatusBadRequest)
return
}
admin, err := app.GetUser(r)
if err != nil {
app.clientError(w, http.StatusUnauthorized)
return
}
var form forms.DeleteEmailForm
if err := app.decodePostForm(r, &form); err != nil {
app.clientError(w, http.StatusBadRequest)
return
}
if err := form.Validate(); err != nil {
panic(err)
}
2022-06-04 11:53:07 +00:00
if !form.Valid() {
form.EmailAddress = emailAddress
form.User = userToEdit
2022-09-26 09:58:36 +00:00
data := app.newTemplateData(r.Context(), r, topLevelNavUsers, subLevelNavUsers)
2022-06-04 11:53:07 +00:00
data.Form = &form
2022-06-04 11:53:07 +00:00
app.render(w, http.StatusUnprocessableEntity, "delete_email.html", data)
return
}
form.Normalize()
if err := app.users.DeleteEmail(r.Context(), userToEdit, admin, emailAddress, form.Reasoning); err != nil {
panic(err)
2022-06-04 11:53:07 +00:00
}
2022-09-26 09:58:36 +00:00
app.addFlash(r.Context(), &FlashMessage{
2022-06-04 11:53:07 +00:00
Variant: flashWarning,
Title: "Email address deleted",
Message: fmt.Sprintf(
"Deleted email address %s of user %s",
form.EmailAddress,
userToEdit.Name,
),
})
http.Redirect(w, r, fmt.Sprintf("/users/%d/", userToEdit.ID), http.StatusSeeOther)
2022-06-02 21:14:38 +00:00
}
func (app *application) newUserForm(w http.ResponseWriter, r *http.Request) {
2022-09-26 09:58:36 +00:00
data := app.newTemplateData(r.Context(), r, topLevelNavUsers, subLevelNavUsers)
data.Form = &forms.NewUserForm{}
app.render(w, http.StatusOK, "create_user.html", data)
2022-06-02 21:14:38 +00:00
}
func (app *application) newUserSubmit(w http.ResponseWriter, r *http.Request) {
admin, err := app.GetUser(r)
if err != nil {
app.clientError(w, http.StatusUnauthorized)
return
}
var form forms.NewUserForm
if err := app.decodePostForm(r, &form); err != nil {
app.clientError(w, http.StatusBadRequest)
return
}
if err := form.Validate(); err != nil {
panic(err)
}
if !form.Valid() {
2022-09-26 09:58:36 +00:00
data := app.newTemplateData(r.Context(), r, topLevelNavUsers, subLevelNavUsers)
data.Form = &form
app.render(w, http.StatusUnprocessableEntity, "create_user.html", data)
return
}
form.Normalize()
createUserParams := &models.CreateUserParams{
Admin: admin,
Name: form.Name,
Reminder: form.EmailAddress,
Emails: []string{form.EmailAddress},
Reasoning: form.Reasoning,
}
userID, err := app.users.Create(r.Context(), createUserParams)
if err != nil {
panic(err)
}
2022-09-26 09:58:36 +00:00
app.addFlash(r.Context(), &FlashMessage{
Variant: flashSuccess,
Title: "User created",
Message: fmt.Sprintf("Created new user %s", form.Name),
})
http.Redirect(w, r, fmt.Sprintf("/users/%d", userID), http.StatusSeeOther)
2022-06-02 21:14:38 +00:00
}
func (app *application) deleteUserForm(w http.ResponseWriter, r *http.Request) {
2022-09-26 09:58:36 +00:00
userToDelete := app.userFromRequestParam(r.Context(), w, r, app.users.CanDelete())
if userToDelete == nil {
return
}
if !userToDelete.CanDelete() {
app.notFound(w)
return
}
2022-09-26 09:58:36 +00:00
data := app.newTemplateData(r.Context(), r, topLevelNavUsers, subLevelNavUsers)
data.Form = &forms.DeleteUserForm{
User: userToDelete,
}
app.render(w, http.StatusOK, "delete_user.html", data)
}
func (app *application) deleteUserSubmit(w http.ResponseWriter, r *http.Request) {
2022-09-26 09:58:36 +00:00
userToDelete := app.userFromRequestParam(r.Context(), w, r, app.users.CanDelete())
if userToDelete == nil {
return
}
if !userToDelete.CanDelete() {
app.notFound(w)
return
}
var form forms.DeleteUserForm
err := app.decodePostForm(r, &form)
if err != nil {
app.clientError(w, http.StatusBadRequest)
return
}
form.User = userToDelete
if err := form.Validate(); err != nil {
panic(err)
}
2022-09-26 09:58:36 +00:00
data := app.newTemplateData(r.Context(), r, topLevelNavUsers, subLevelNavUsers)
if !form.Valid() {
data.Form = &form
app.render(w, http.StatusUnprocessableEntity, "delete_user.html", data)
return
}
form.Normalize()
if err = app.users.DeleteUser(r.Context(), userToDelete, data.User, form.Reasoning); err != nil {
panic(err)
}
2022-09-26 09:58:36 +00:00
app.addFlash(r.Context(), &FlashMessage{
2022-06-04 11:52:53 +00:00
Variant: flashWarning,
Title: "User deleted",
Message: fmt.Sprintf("User %s has been deleted.", userToDelete.Name),
})
http.Redirect(w, r, "/users/", http.StatusSeeOther)
2022-05-09 19:09:24 +00:00
}
func (app *application) chooseVotersForm(w http.ResponseWriter, r *http.Request) {
2022-09-26 09:58:36 +00:00
data := app.newTemplateData(r.Context(), r, topLevelNavUsers, subLevelNavVoters)
allUsers, err := app.users.List(r.Context(), app.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,
}
app.render(w, http.StatusOK, "choose_voters.html", data)
}
func (app *application) chooseVotersSubmit(w http.ResponseWriter, r *http.Request) {
admin, err := app.GetUser(r)
if err != nil {
app.clientError(w, http.StatusUnauthorized)
return
}
var form forms.ChooseVoterForm
if err = app.decodePostForm(r, &form); err != nil {
app.clientError(w, http.StatusBadRequest)
return
}
if err = form.Validate(); err != nil {
panic(err)
}
if !form.Valid() {
2022-09-26 09:58:36 +00:00
data := app.newTemplateData(r.Context(), r, topLevelNavUsers, subLevelNavVoters)
allUsers, err := app.users.List(r.Context(), app.users.WithRoles())
if err != nil {
panic(err)
}
form.Users = allUsers
data.Form = &form
app.render(w, http.StatusUnprocessableEntity, "choose_voters.html", data)
return
}
form.Normalize()
if err = app.users.ChooseVoters(r.Context(), admin, form.VoterIDs, form.Reasoning); err != nil {
panic(err)
}
2022-09-26 09:58:36 +00:00
app.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 (app *application) healthCheck(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")
2022-05-29 13:43:45 +00:00
var (
err error
hasErrors = false
)
if err = app.mailNotifier.Ping(); err != nil {
hasErrors = true
response.Mail = failed
}
if err = app.motions.DB.Ping(); err != nil {
hasErrors = true
response.DB = failed
}
if hasErrors {
w.WriteHeader(http.StatusInternalServerError)
} else {
w.WriteHeader(http.StatusOK)
}
_ = enc.Encode(response)
}