Implement user creation and voter management
This commit is contained in:
parent
39bd724381
commit
f966cbd62f
14 changed files with 599 additions and 263 deletions
|
@ -48,6 +48,7 @@ const (
|
|||
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 {
|
||||
|
@ -75,12 +76,6 @@ func (m *templateData) motionPaginationOptions(limit int, first, last *time.Time
|
|||
}
|
||||
|
||||
func (app *application) motionList(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/motions/" {
|
||||
app.notFound(w)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
var (
|
||||
listOptions *models.MotionListOptions
|
||||
err error
|
||||
|
@ -97,16 +92,12 @@ func (app *application) motionList(w http.ResponseWriter, r *http.Request) {
|
|||
|
||||
motions, err := app.motions.List(ctx, listOptions)
|
||||
if err != nil {
|
||||
app.serverError(w, err)
|
||||
|
||||
return
|
||||
panic(err)
|
||||
}
|
||||
|
||||
first, last, err := app.motions.TimestampRange(ctx, listOptions)
|
||||
if err != nil {
|
||||
app.serverError(w, err)
|
||||
|
||||
return
|
||||
panic(err)
|
||||
}
|
||||
|
||||
templateData := app.newTemplateData(r, "motions", "all-motions")
|
||||
|
@ -119,9 +110,7 @@ func (app *application) motionList(w http.ResponseWriter, r *http.Request) {
|
|||
|
||||
err = templateData.motionPaginationOptions(listOptions.Limit, first, last)
|
||||
if err != nil {
|
||||
app.serverError(w, err)
|
||||
|
||||
return
|
||||
panic(err)
|
||||
}
|
||||
|
||||
app.render(w, http.StatusOK, "motions.html", templateData)
|
||||
|
@ -207,17 +196,21 @@ func (app *application) newMotionSubmit(w http.ResponseWriter, r *http.Request)
|
|||
return
|
||||
}
|
||||
|
||||
form.Validate()
|
||||
if err := form.Validate(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if !form.Valid() {
|
||||
data := app.newTemplateData(r, topLevelNavMotions, subLevelNavMotionsAll)
|
||||
data.Form = form
|
||||
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)
|
||||
|
@ -238,16 +231,12 @@ func (app *application) newMotionSubmit(w http.ResponseWriter, r *http.Request)
|
|||
now.Add(dueDuration),
|
||||
)
|
||||
if err != nil {
|
||||
app.serverError(w, err)
|
||||
|
||||
return
|
||||
panic(err)
|
||||
}
|
||||
|
||||
decision, err := app.motions.ByID(r.Context(), decisionID)
|
||||
if err != nil {
|
||||
app.serverError(w, err)
|
||||
|
||||
return
|
||||
panic(err)
|
||||
}
|
||||
|
||||
app.mailNotifier.Notify(&NewDecisionNotification{
|
||||
|
@ -300,11 +289,13 @@ func (app *application) editMotionSubmit(w http.ResponseWriter, r *http.Request)
|
|||
return
|
||||
}
|
||||
|
||||
form.Validate()
|
||||
if err := form.Validate(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if !form.Valid() {
|
||||
data := app.newTemplateData(r, topLevelNavMotions, subLevelNavMotionsAll)
|
||||
data.Form = form
|
||||
data.Form = &form
|
||||
|
||||
app.render(w, http.StatusUnprocessableEntity, "edit_motion.html", data)
|
||||
|
||||
|
@ -334,16 +325,12 @@ func (app *application) editMotionSubmit(w http.ResponseWriter, r *http.Request)
|
|||
},
|
||||
)
|
||||
if err != nil {
|
||||
app.serverError(w, err)
|
||||
|
||||
return
|
||||
panic(err)
|
||||
}
|
||||
|
||||
decision, err := app.motions.ByID(r.Context(), motion.ID)
|
||||
if err != nil {
|
||||
app.serverError(w, err)
|
||||
|
||||
return
|
||||
panic(err)
|
||||
}
|
||||
|
||||
app.mailNotifier.Notify(&UpdateDecisionNotification{
|
||||
|
@ -395,9 +382,7 @@ func (app *application) withdrawMotionSubmit(w http.ResponseWriter, r *http.Requ
|
|||
err = app.motions.Withdraw(r.Context(), motion.ID)
|
||||
|
||||
if err != nil {
|
||||
app.serverError(w, err)
|
||||
|
||||
return
|
||||
panic(err)
|
||||
}
|
||||
|
||||
app.mailNotifier.Notify(&WithDrawMotionNotification{motion, user})
|
||||
|
@ -459,9 +444,7 @@ func (app *application) voteSubmit(w http.ResponseWriter, r *http.Request) {
|
|||
|
||||
clientCert, err := getPEMClientCert(r)
|
||||
if err != nil {
|
||||
app.serverError(w, err)
|
||||
|
||||
return
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if err := app.motions.UpdateVote(r.Context(), user.ID, motion.ID, func(v *models.Vote) {
|
||||
|
@ -469,9 +452,7 @@ func (app *application) voteSubmit(w http.ResponseWriter, r *http.Request) {
|
|||
v.Voted = time.Now().UTC()
|
||||
v.Notes = fmt.Sprintf("Direct Vote\n\n%s", clientCert)
|
||||
}); err != nil {
|
||||
app.serverError(w, err)
|
||||
|
||||
return
|
||||
panic(err)
|
||||
}
|
||||
|
||||
app.mailNotifier.Notify(&DirectVoteNotification{
|
||||
|
@ -499,9 +480,7 @@ func (app *application) proxyVoteForm(w http.ResponseWriter, r *http.Request) {
|
|||
|
||||
potentialVoters, err := app.users.Voters(r.Context())
|
||||
if err != nil {
|
||||
app.serverError(w, err)
|
||||
|
||||
return
|
||||
panic(err)
|
||||
}
|
||||
|
||||
data.Form = &forms.ProxyVoteForm{
|
||||
|
@ -526,7 +505,9 @@ func (app *application) proxyVoteSubmit(w http.ResponseWriter, r *http.Request)
|
|||
return
|
||||
}
|
||||
|
||||
form.Validate()
|
||||
if err := form.Validate(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if !form.Valid() {
|
||||
data := app.newTemplateData(r, topLevelNavMotions, subLevelNavMotionsAll)
|
||||
|
@ -534,13 +515,11 @@ func (app *application) proxyVoteSubmit(w http.ResponseWriter, r *http.Request)
|
|||
|
||||
potentialVoters, err := app.users.Voters(r.Context())
|
||||
if err != nil {
|
||||
app.serverError(w, err)
|
||||
|
||||
return
|
||||
panic(err)
|
||||
}
|
||||
|
||||
form.Voters = potentialVoters
|
||||
data.Form = form
|
||||
data.Form = &form
|
||||
|
||||
app.render(w, http.StatusUnprocessableEntity, "proxy_vote.html", data)
|
||||
|
||||
|
@ -558,9 +537,7 @@ func (app *application) proxyVoteSubmit(w http.ResponseWriter, r *http.Request)
|
|||
|
||||
clientCert, err := getPEMClientCert(r)
|
||||
if err != nil {
|
||||
app.serverError(w, err)
|
||||
|
||||
return
|
||||
panic(err)
|
||||
}
|
||||
|
||||
voter := app.getVoter(w, r, form.Voter.ID)
|
||||
|
@ -573,9 +550,7 @@ func (app *application) proxyVoteSubmit(w http.ResponseWriter, r *http.Request)
|
|||
v.Voted = time.Now().UTC()
|
||||
v.Notes = fmt.Sprintf("Proxy-Vote by %s\n\n%s\n\n%s", user.Name, form.Justification, clientCert)
|
||||
}); err != nil {
|
||||
app.serverError(w, err)
|
||||
|
||||
return
|
||||
panic(err)
|
||||
}
|
||||
|
||||
app.mailNotifier.Notify(&ProxyVoteNotification{
|
||||
|
@ -604,9 +579,7 @@ func (app *application) userList(w http.ResponseWriter, r *http.Request) {
|
|||
app.users.CanDelete(),
|
||||
)
|
||||
if err != nil {
|
||||
app.serverError(w, err)
|
||||
|
||||
return
|
||||
panic(err)
|
||||
}
|
||||
|
||||
data.Users = users
|
||||
|
@ -622,16 +595,12 @@ func (app *application) editUserForm(w http.ResponseWriter, r *http.Request) {
|
|||
|
||||
roles, err := userToEdit.Roles()
|
||||
if err != nil {
|
||||
app.serverError(w, err)
|
||||
|
||||
return
|
||||
panic(err)
|
||||
}
|
||||
|
||||
emailAddresses, err := userToEdit.EmailAddresses()
|
||||
if err != nil {
|
||||
app.serverError(w, err)
|
||||
|
||||
return
|
||||
panic(err)
|
||||
}
|
||||
|
||||
roleNames := make([]string, len(roles))
|
||||
|
@ -673,9 +642,7 @@ func (app *application) editUserSubmit(w http.ResponseWriter, r *http.Request) {
|
|||
form.AllRoles = models.AllRoles
|
||||
|
||||
if err := form.Validate(); err != nil {
|
||||
app.serverError(w, err)
|
||||
|
||||
return
|
||||
panic(err)
|
||||
}
|
||||
|
||||
data := app.newTemplateData(r, topLevelNavUsers, subLevelNavUsers)
|
||||
|
@ -683,16 +650,12 @@ func (app *application) editUserSubmit(w http.ResponseWriter, r *http.Request) {
|
|||
if !form.Valid() {
|
||||
roles, err := userToEdit.Roles()
|
||||
if err != nil {
|
||||
app.serverError(w, err)
|
||||
|
||||
return
|
||||
panic(err)
|
||||
}
|
||||
|
||||
emailAddresses, err := userToEdit.EmailAddresses()
|
||||
if err != nil {
|
||||
app.serverError(w, err)
|
||||
|
||||
return
|
||||
panic(err)
|
||||
}
|
||||
|
||||
roleNames := make([]string, len(roles))
|
||||
|
@ -714,9 +677,7 @@ func (app *application) editUserSubmit(w http.ResponseWriter, r *http.Request) {
|
|||
form.UpdateUser(userToEdit)
|
||||
|
||||
if err := app.users.EditUser(r.Context(), userToEdit, data.User, form.Roles, form.Reasoning); err != nil {
|
||||
app.serverError(w, err)
|
||||
|
||||
return
|
||||
panic(err)
|
||||
}
|
||||
|
||||
app.addFlash(r, &FlashMessage{
|
||||
|
@ -738,9 +699,7 @@ func (app *application) userAddEmailForm(w http.ResponseWriter, r *http.Request)
|
|||
|
||||
emailAddresses, err := userToEdit.EmailAddresses()
|
||||
if err != nil {
|
||||
app.serverError(w, err)
|
||||
|
||||
return
|
||||
panic(err)
|
||||
}
|
||||
|
||||
data := app.newTemplateData(r, topLevelNavUsers, subLevelNavUsers)
|
||||
|
@ -771,16 +730,16 @@ func (app *application) userAddEmailSubmit(w http.ResponseWriter, r *http.Reques
|
|||
|
||||
form.User = userToEdit
|
||||
|
||||
form.Validate()
|
||||
if err := form.Validate(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
data := app.newTemplateData(r, topLevelNavUsers, subLevelNavUsers)
|
||||
|
||||
if form.Valid() {
|
||||
emailExists, err := app.users.EmailExists(r.Context(), form.EmailAddress)
|
||||
if err != nil {
|
||||
app.serverError(w, err)
|
||||
|
||||
return
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if emailExists {
|
||||
|
@ -793,9 +752,7 @@ func (app *application) userAddEmailSubmit(w http.ResponseWriter, r *http.Reques
|
|||
if !form.Valid() {
|
||||
emailAddresses, err := userToEdit.EmailAddresses()
|
||||
if err != nil {
|
||||
app.serverError(w, err)
|
||||
|
||||
return
|
||||
panic(err)
|
||||
}
|
||||
|
||||
form.EmailAddresses = emailAddresses
|
||||
|
@ -810,9 +767,7 @@ func (app *application) userAddEmailSubmit(w http.ResponseWriter, r *http.Reques
|
|||
form.Normalize()
|
||||
|
||||
if err := app.users.AddEmail(r.Context(), userToEdit, data.User, form.EmailAddress, form.Reasoning); err != nil {
|
||||
app.serverError(w, err)
|
||||
|
||||
return
|
||||
panic(err)
|
||||
}
|
||||
|
||||
app.addFlash(r, &FlashMessage{
|
||||
|
@ -831,9 +786,7 @@ func (app *application) userAddEmailSubmit(w http.ResponseWriter, r *http.Reques
|
|||
func (app *application) userDeleteEmailForm(w http.ResponseWriter, r *http.Request) {
|
||||
userToEdit, emailAddress, err := app.deleteEmailParams(w, r)
|
||||
if err != nil {
|
||||
app.serverError(w, err)
|
||||
|
||||
return
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if userToEdit == nil || emailAddress == "" {
|
||||
|
@ -862,9 +815,7 @@ func (app *application) userDeleteEmailForm(w http.ResponseWriter, r *http.Reque
|
|||
func (app *application) userDeleteEmailSubmit(w http.ResponseWriter, r *http.Request) {
|
||||
userToEdit, emailAddress, err := app.deleteEmailParams(w, r)
|
||||
if err != nil {
|
||||
app.serverError(w, err)
|
||||
|
||||
return
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if userToEdit == nil || emailAddress == "" {
|
||||
|
@ -895,7 +846,9 @@ func (app *application) userDeleteEmailSubmit(w http.ResponseWriter, r *http.Req
|
|||
return
|
||||
}
|
||||
|
||||
form.Validate()
|
||||
if err := form.Validate(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if !form.Valid() {
|
||||
form.EmailAddress = emailAddress
|
||||
|
@ -903,7 +856,7 @@ func (app *application) userDeleteEmailSubmit(w http.ResponseWriter, r *http.Req
|
|||
|
||||
data := app.newTemplateData(r, topLevelNavUsers, subLevelNavUsers)
|
||||
|
||||
data.Form = form
|
||||
data.Form = &form
|
||||
|
||||
app.render(w, http.StatusUnprocessableEntity, "delete_email.html", data)
|
||||
|
||||
|
@ -913,9 +866,7 @@ func (app *application) userDeleteEmailSubmit(w http.ResponseWriter, r *http.Req
|
|||
form.Normalize()
|
||||
|
||||
if err := app.users.DeleteEmail(r.Context(), userToEdit, admin, emailAddress, form.Reasoning); err != nil {
|
||||
app.serverError(w, err)
|
||||
|
||||
return
|
||||
panic(err)
|
||||
}
|
||||
|
||||
app.addFlash(r, &FlashMessage{
|
||||
|
@ -931,14 +882,66 @@ func (app *application) userDeleteEmailSubmit(w http.ResponseWriter, r *http.Req
|
|||
http.Redirect(w, r, fmt.Sprintf("/users/%d/", userToEdit.ID), http.StatusSeeOther)
|
||||
}
|
||||
|
||||
func (app *application) newUserForm(_ http.ResponseWriter, _ *http.Request) {
|
||||
// TODO: implement newUserForm
|
||||
panic("not implemented")
|
||||
func (app *application) newUserForm(w http.ResponseWriter, r *http.Request) {
|
||||
data := app.newTemplateData(r, topLevelNavUsers, subLevelNavUsers)
|
||||
|
||||
data.Form = &forms.NewUserForm{}
|
||||
|
||||
app.render(w, http.StatusOK, "create_user.html", data)
|
||||
}
|
||||
|
||||
func (app *application) newUserSubmit(_ http.ResponseWriter, _ *http.Request) {
|
||||
// TODO: implement newUserSubmit
|
||||
panic("not implemented")
|
||||
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() {
|
||||
data := app.newTemplateData(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)
|
||||
}
|
||||
|
||||
app.addFlash(r, &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)
|
||||
}
|
||||
|
||||
func (app *application) deleteUserForm(w http.ResponseWriter, r *http.Request) {
|
||||
|
@ -985,7 +988,9 @@ func (app *application) deleteUserSubmit(w http.ResponseWriter, r *http.Request)
|
|||
|
||||
form.User = userToDelete
|
||||
|
||||
form.Validate()
|
||||
if err := form.Validate(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
data := app.newTemplateData(r, topLevelNavUsers, subLevelNavUsers)
|
||||
|
||||
|
@ -1000,9 +1005,7 @@ func (app *application) deleteUserSubmit(w http.ResponseWriter, r *http.Request)
|
|||
form.Normalize()
|
||||
|
||||
if err = app.users.DeleteUser(r.Context(), userToDelete, data.User, form.Reasoning); err != nil {
|
||||
app.serverError(w, err)
|
||||
|
||||
return
|
||||
panic(err)
|
||||
}
|
||||
|
||||
app.addFlash(r, &FlashMessage{
|
||||
|
@ -1014,6 +1017,89 @@ func (app *application) deleteUserSubmit(w http.ResponseWriter, r *http.Request)
|
|||
http.Redirect(w, r, "/users/", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
func (app *application) chooseVotersForm(w http.ResponseWriter, r *http.Request) {
|
||||
data := app.newTemplateData(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() {
|
||||
data := app.newTemplateData(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)
|
||||
}
|
||||
|
||||
app.addFlash(r, &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"
|
||||
|
|
|
@ -27,10 +27,10 @@ import (
|
|||
"io/fs"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"runtime/debug"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"git.cacert.org/cacert-boardvoting/internal/forms"
|
||||
"github.com/Masterminds/sprig/v3"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-playground/form/v4"
|
||||
|
@ -40,14 +40,6 @@ import (
|
|||
"git.cacert.org/cacert-boardvoting/ui"
|
||||
)
|
||||
|
||||
func (app *application) serverError(w http.ResponseWriter, err error) {
|
||||
trace := fmt.Sprintf("%s\n%s", err.Error(), debug.Stack())
|
||||
|
||||
_ = app.errorLog.Output(2, trace)
|
||||
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
func (app *application) clientError(w http.ResponseWriter, status int) {
|
||||
http.Error(w, http.StatusText(status), status)
|
||||
}
|
||||
|
@ -56,7 +48,7 @@ func (app *application) notFound(w http.ResponseWriter) {
|
|||
app.clientError(w, http.StatusNotFound)
|
||||
}
|
||||
|
||||
func (app *application) decodePostForm(r *http.Request, dst any) error {
|
||||
func (app *application) decodePostForm(r *http.Request, dst forms.Form) error {
|
||||
err := r.ParseForm()
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not parse HTML form: %w", err)
|
||||
|
@ -127,7 +119,7 @@ type templateData struct {
|
|||
Users []*models.User
|
||||
Request *http.Request
|
||||
Flashes []FlashMessage
|
||||
Form any
|
||||
Form forms.Form
|
||||
ActiveNav topLevelNavItem
|
||||
ActiveSubNav subLevelNavItem
|
||||
CSRFToken string
|
||||
|
@ -153,18 +145,14 @@ func (app *application) newTemplateData(
|
|||
func (app *application) render(w http.ResponseWriter, status int, page string, data *templateData) {
|
||||
ts, ok := app.templateCache[page]
|
||||
if !ok {
|
||||
app.serverError(w, fmt.Errorf("the template %s does not exist", page))
|
||||
|
||||
return
|
||||
panic(fmt.Sprintf("the template %s does not exist", page))
|
||||
}
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
|
||||
err := ts.ExecuteTemplate(buf, "base", data)
|
||||
if err != nil {
|
||||
app.serverError(w, err)
|
||||
|
||||
return
|
||||
panic(err)
|
||||
}
|
||||
|
||||
w.WriteHeader(status)
|
||||
|
@ -180,9 +168,7 @@ func (app *application) motionFromRequestParam(
|
|||
|
||||
motion, err := app.motions.ByTag(r.Context(), chi.URLParam(r, "tag"), withVotes)
|
||||
if err != nil {
|
||||
app.serverError(w, err)
|
||||
|
||||
return nil
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if motion.ID == 0 {
|
||||
|
@ -206,9 +192,7 @@ func (app *application) userFromRequestParam(
|
|||
|
||||
user, err := app.users.ByID(r.Context(), int64(userID), options...)
|
||||
if err != nil {
|
||||
app.serverError(w, err)
|
||||
|
||||
return nil
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if user == nil {
|
||||
|
@ -286,7 +270,6 @@ const (
|
|||
flashWarning FlashVariant = "warning"
|
||||
flashInfo FlashVariant = "info"
|
||||
flashSuccess FlashVariant = "success"
|
||||
flashError FlashVariant = "error"
|
||||
)
|
||||
|
||||
type FlashMessage struct {
|
||||
|
|
|
@ -215,17 +215,13 @@ func (app *application) startHTTPSServer(config *Config) error {
|
|||
func (app *application) getVoter(w http.ResponseWriter, r *http.Request, voterID int64) *models.User {
|
||||
voter, err := app.users.ByID(r.Context(), voterID, app.users.WithRoles())
|
||||
if err != nil {
|
||||
app.serverError(w, err)
|
||||
|
||||
return nil
|
||||
panic(err)
|
||||
}
|
||||
|
||||
var isVoter bool
|
||||
|
||||
if isVoter, err = voter.HasRole(models.RoleVoter); err != nil {
|
||||
app.serverError(w, err)
|
||||
|
||||
return nil
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if !isVoter {
|
||||
|
|
|
@ -90,9 +90,7 @@ func (app *application) tryAuthenticate(next http.Handler) http.Handler {
|
|||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
user, cert, err := app.authenticateRequest(r)
|
||||
if err != nil {
|
||||
app.serverError(w, err)
|
||||
|
||||
return
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if user == nil {
|
||||
|
@ -161,9 +159,7 @@ func (app *application) requireRole(next http.Handler, roles ...models.RoleName)
|
|||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
hasRole, hasUser, err := app.HasRole(r, roles...)
|
||||
if err != nil {
|
||||
app.serverError(w, err)
|
||||
|
||||
return
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if !hasUser {
|
||||
|
|
|
@ -21,6 +21,7 @@ import (
|
|||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"database/sql"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
|
@ -67,9 +68,16 @@ func TestApplication_tryAuthenticate(t *testing.T) {
|
|||
|
||||
_, err = users.Create(
|
||||
context.Background(),
|
||||
"Test User",
|
||||
"test@example.org",
|
||||
[]string{"test@example.org"},
|
||||
&models.CreateUserParams{
|
||||
Admin: &models.User{
|
||||
Name: "Admin",
|
||||
Reminder: sql.NullString{String: "admin@example.org", Valid: true},
|
||||
},
|
||||
Name: "Test User",
|
||||
Reminder: "test@example.org",
|
||||
Emails: []string{"test@example.org"},
|
||||
Reasoning: "Test data",
|
||||
},
|
||||
)
|
||||
|
||||
var nextCtx context.Context
|
||||
|
|
|
@ -104,6 +104,9 @@ func (app *application) routes() http.Handler {
|
|||
r.Get("/delete", app.deleteUserForm)
|
||||
r.Post("/delete", app.deleteUserSubmit)
|
||||
})
|
||||
|
||||
r.Get("/voters/", app.chooseVotersForm)
|
||||
r.Post("/voters/", app.chooseVotersSubmit)
|
||||
})
|
||||
})
|
||||
|
||||
|
|
|
@ -39,6 +39,68 @@ const (
|
|||
threeWeeks = 28
|
||||
)
|
||||
|
||||
type Form interface {
|
||||
Validate() error
|
||||
Normalize()
|
||||
}
|
||||
|
||||
func validateUserName(v *validator.Validator, name string, field string) {
|
||||
v.CheckField(validator.NotBlank(name), field, "This field cannot be blank")
|
||||
}
|
||||
|
||||
func validateEmailAddress(v *validator.Validator, address string, field string) {
|
||||
v.CheckField(validator.NotBlank(address), field, "This field cannot be blank")
|
||||
v.CheckField(validator.IsEmail(address), field, "This field must be an email address")
|
||||
}
|
||||
|
||||
func validateReasoning(v *validator.Validator, reasoning string, field string) {
|
||||
v.CheckField(validator.NotBlank(reasoning), field, "This field cannot be blank")
|
||||
v.CheckField(
|
||||
validator.MinChars(reasoning, minimumJustificationLen),
|
||||
field,
|
||||
fmt.Sprintf("This field must be at least %d characters long", minimumJustificationLen),
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
func validateMotionTitle(v *validator.Validator, title string, field string) {
|
||||
v.CheckField(
|
||||
validator.NotBlank(title),
|
||||
field,
|
||||
"This field cannot be blank",
|
||||
)
|
||||
v.CheckField(
|
||||
validator.MinChars(title, minimumTitleLength),
|
||||
field,
|
||||
fmt.Sprintf("This field must be at least %d characters long", minimumTitleLength),
|
||||
)
|
||||
v.CheckField(
|
||||
validator.MaxChars(title, maximumTitleLength),
|
||||
field,
|
||||
fmt.Sprintf("This field must be at most %d characters long", maximumTitleLength),
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
func validateMotionContent(v *validator.Validator, content string, field string) {
|
||||
v.CheckField(
|
||||
validator.NotBlank(content),
|
||||
field,
|
||||
"This field cannot be blank",
|
||||
)
|
||||
v.CheckField(
|
||||
validator.MinChars(content, minimumContentLength),
|
||||
field,
|
||||
fmt.Sprintf("This field must be at least %d characters long", minimumContentLength),
|
||||
)
|
||||
v.CheckField(
|
||||
validator.MaxChars(content, maximumContentLength),
|
||||
field,
|
||||
fmt.Sprintf("This field must be at most %d characters long", maximumContentLength),
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
type EditMotionForm struct {
|
||||
Title string `form:"title"`
|
||||
Content string `form:"content"`
|
||||
|
@ -47,42 +109,16 @@ type EditMotionForm struct {
|
|||
validator.Validator `form:"-"`
|
||||
}
|
||||
|
||||
func (f *EditMotionForm) Validate() {
|
||||
f.CheckField(
|
||||
validator.NotBlank(f.Title),
|
||||
"title",
|
||||
"This field cannot be blank",
|
||||
)
|
||||
f.CheckField(
|
||||
validator.MinChars(f.Title, minimumTitleLength),
|
||||
"title",
|
||||
fmt.Sprintf("This field must be at least %d characters long", minimumTitleLength),
|
||||
)
|
||||
f.CheckField(
|
||||
validator.MaxChars(f.Title, maximumTitleLength),
|
||||
"title",
|
||||
fmt.Sprintf("This field must be at most %d characters long", maximumTitleLength),
|
||||
)
|
||||
f.CheckField(
|
||||
validator.NotBlank(f.Content),
|
||||
"content",
|
||||
"This field cannot be blank",
|
||||
)
|
||||
f.CheckField(
|
||||
validator.MinChars(f.Content, minimumContentLength),
|
||||
"content",
|
||||
fmt.Sprintf("This field must be at least %d characters long", minimumContentLength),
|
||||
)
|
||||
f.CheckField(
|
||||
validator.MaxChars(f.Content, maximumContentLength),
|
||||
"content",
|
||||
fmt.Sprintf("This field must be at most %d characters long", maximumContentLength),
|
||||
)
|
||||
func (f *EditMotionForm) Validate() error {
|
||||
validateMotionTitle(&f.Validator, f.Title, "title")
|
||||
validateMotionContent(&f.Validator, f.Content, "content")
|
||||
f.CheckField(validator.NotNil(f.Type), "type", "You must choose a valid vote type")
|
||||
|
||||
f.CheckField(validator.PermittedInt(
|
||||
f.Due, threeDays, oneWeek, twoWeeks, threeWeeks), "due", "invalid duration choice",
|
||||
)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *EditMotionForm) Normalize() {
|
||||
|
@ -94,6 +130,10 @@ type DirectVoteForm struct {
|
|||
Choice *models.VoteChoice
|
||||
}
|
||||
|
||||
func (f *DirectVoteForm) Validate() error { return nil }
|
||||
|
||||
func (f *DirectVoteForm) Normalize() {}
|
||||
|
||||
type ProxyVoteForm struct {
|
||||
Voter *models.User `form:"voter"`
|
||||
Choice *models.VoteChoice `form:"choice"`
|
||||
|
@ -102,18 +142,12 @@ type ProxyVoteForm struct {
|
|||
validator.Validator `form:"-"`
|
||||
}
|
||||
|
||||
func (f *ProxyVoteForm) Validate() {
|
||||
func (f *ProxyVoteForm) Validate() error {
|
||||
f.CheckField(validator.NotNil(f.Voter), "voter", "Please choose a valid voter")
|
||||
f.CheckField(validator.NotBlank(f.Justification), "justification", "This field cannot be blank")
|
||||
f.CheckField(
|
||||
validator.MinChars(
|
||||
f.Justification,
|
||||
minimumJustificationLen,
|
||||
),
|
||||
"justification",
|
||||
fmt.Sprintf("This field must be at least %d characters long", minimumJustificationLen),
|
||||
)
|
||||
validateReasoning(&f.Validator, f.Justification, "justification")
|
||||
f.CheckField(validator.NotNil(f.Choice), "choice", "A choice has to be made")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *ProxyVoteForm) Normalize() {
|
||||
|
@ -137,7 +171,7 @@ func (f *EditUserForm) Validate() error {
|
|||
return fmt.Errorf("error while validating form: %w", err)
|
||||
}
|
||||
|
||||
f.CheckField(validator.NotBlank(f.Name), "name", "This field cannot be blank")
|
||||
validateUserName(&f.Validator, f.Name, "name")
|
||||
|
||||
f.CheckField(
|
||||
validator.NotBlank(f.ReminderMail),
|
||||
|
@ -161,12 +195,7 @@ func (f *EditUserForm) Validate() error {
|
|||
fmt.Sprintf("Roles must only contain values from %s", strings.Join(allRoleNames, ", ")),
|
||||
)
|
||||
|
||||
f.CheckField(validator.NotBlank(f.Reasoning), "reasoning", "This field cannot be blank")
|
||||
f.CheckField(
|
||||
validator.MinChars(f.Reasoning, minimumJustificationLen),
|
||||
"reasoning",
|
||||
fmt.Sprintf("This field must be at least %d characters long", minimumJustificationLen),
|
||||
)
|
||||
validateReasoning(&f.Validator, f.Reasoning, "reasoning")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
@ -181,19 +210,41 @@ func (f *EditUserForm) UpdateUser(edit *models.User) {
|
|||
edit.Name = f.Name
|
||||
}
|
||||
|
||||
type NewUserForm struct {
|
||||
Name string `form:"name"`
|
||||
EmailAddress string `form:"email_address"`
|
||||
Reasoning string `form:"reasoning"`
|
||||
validator.Validator `form:"-"`
|
||||
}
|
||||
|
||||
func (f *NewUserForm) Validate() error {
|
||||
validateUserName(&f.Validator, f.Name, "name")
|
||||
validateEmailAddress(&f.Validator, f.EmailAddress, "email_address")
|
||||
validateReasoning(&f.Validator, f.Reasoning, "reasoning")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *NewUserForm) Normalize() {
|
||||
f.Name = strings.TrimSpace(f.Name)
|
||||
f.EmailAddress = strings.TrimSpace(f.EmailAddress)
|
||||
f.Reasoning = strings.TrimSpace(f.Reasoning)
|
||||
}
|
||||
|
||||
func (f *NewUserForm) FillUser() *models.User {
|
||||
return &models.User{Name: f.Name}
|
||||
}
|
||||
|
||||
type DeleteUserForm struct {
|
||||
User *models.User `form:"user"`
|
||||
Reasoning string `form:"reasoning"`
|
||||
validator.Validator `form:"-"`
|
||||
}
|
||||
|
||||
func (f *DeleteUserForm) Validate() {
|
||||
f.CheckField(validator.NotBlank(f.Reasoning), "reasoning", "This field cannot be blank")
|
||||
f.CheckField(
|
||||
validator.MinChars(f.Reasoning, minimumJustificationLen),
|
||||
"reasoning",
|
||||
fmt.Sprintf("This field must be at least %d characters long", minimumJustificationLen),
|
||||
)
|
||||
func (f *DeleteUserForm) Validate() error {
|
||||
validateReasoning(&f.Validator, f.Reasoning, "reasoning")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *DeleteUserForm) Normalize() {
|
||||
|
@ -203,20 +254,16 @@ func (f *DeleteUserForm) Normalize() {
|
|||
type AddEmailForm struct {
|
||||
EmailAddress string `form:"email_address"`
|
||||
Reasoning string `form:"reasoning"`
|
||||
User *models.User `form:"-""`
|
||||
User *models.User `form:"-"`
|
||||
EmailAddresses []string `form:"-"`
|
||||
validator.Validator `form:"-"`
|
||||
}
|
||||
|
||||
func (f *AddEmailForm) Validate() {
|
||||
f.CheckField(validator.NotBlank(f.EmailAddress), "email_address", "This field cannot be blank")
|
||||
f.CheckField(validator.IsEmail(f.EmailAddress), "email_address", "This field must be an email address")
|
||||
f.CheckField(validator.NotBlank(f.Reasoning), "reasoning", "This field cannot be blank")
|
||||
f.CheckField(
|
||||
validator.MinChars(f.Reasoning, minimumJustificationLen),
|
||||
"reasoning",
|
||||
fmt.Sprintf("This field must be at least %d characters long", minimumJustificationLen),
|
||||
)
|
||||
func (f *AddEmailForm) Validate() error {
|
||||
validateEmailAddress(&f.Validator, f.EmailAddress, "email_address")
|
||||
validateReasoning(&f.Validator, f.Reasoning, "reasoning")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *AddEmailForm) Normalize() {
|
||||
|
@ -231,15 +278,28 @@ type DeleteEmailForm struct {
|
|||
validator.Validator `form:"-"`
|
||||
}
|
||||
|
||||
func (f *DeleteEmailForm) Validate() {
|
||||
f.CheckField(validator.NotBlank(f.Reasoning), "reasoning", "This field cannot be blank")
|
||||
f.CheckField(
|
||||
validator.MinChars(f.Reasoning, minimumJustificationLen),
|
||||
"reasoning",
|
||||
fmt.Sprintf("This field must be at least %d characters long", minimumJustificationLen),
|
||||
)
|
||||
func (f *DeleteEmailForm) Validate() error {
|
||||
validateReasoning(&f.Validator, f.Reasoning, "reasoning")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *DeleteEmailForm) Normalize() {
|
||||
f.Reasoning = strings.TrimSpace(f.Reasoning)
|
||||
}
|
||||
|
||||
type ChooseVoterForm struct {
|
||||
Reasoning string `form:"reasoning"`
|
||||
VoterIDs []int64 `form:"voters"`
|
||||
Users []*models.User
|
||||
validator.Validator `form:"-"`
|
||||
}
|
||||
|
||||
func (f *ChooseVoterForm) Validate() error {
|
||||
validateReasoning(&f.Validator, f.Reasoning, "reasoning")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *ChooseVoterForm) Normalize() {
|
||||
}
|
||||
|
|
|
@ -29,13 +29,12 @@ import (
|
|||
type AuditChange string
|
||||
|
||||
const (
|
||||
AuditCreateUser AuditChange = "CREATE_USER"
|
||||
AuditDeleteUser AuditChange = "DELETE_USER"
|
||||
AuditEditUser AuditChange = "EDIT_USER"
|
||||
AuditAddEmail AuditChange = "ADD_EMAIL"
|
||||
AuditDeleteEmail AuditChange = "DELETE_EMAIL"
|
||||
AuditAddRole AuditChange = "ADD_ROLE"
|
||||
AuditRemoveRole AuditChange = "REMOVE_ROLE"
|
||||
AuditCreateUser AuditChange = "CREATE_USER"
|
||||
AuditDeleteUser AuditChange = "DELETE_USER"
|
||||
AuditEditUser AuditChange = "EDIT_USER"
|
||||
AuditAddEmail AuditChange = "ADD_EMAIL"
|
||||
AuditDeleteEmail AuditChange = "DELETE_EMAIL"
|
||||
AuditChangeVoters AuditChange = "CHANGE_VOTERS"
|
||||
)
|
||||
|
||||
type Audit struct {
|
||||
|
@ -65,3 +64,67 @@ func AuditLog(ctx context.Context, tx *sqlx.Tx, user *User, change AuditChange,
|
|||
|
||||
return nil
|
||||
}
|
||||
|
||||
func emailChangeInfo(before, after []string) any {
|
||||
return struct {
|
||||
Before []string `json:"before"`
|
||||
After []string `json:"after"`
|
||||
}{Before: before, After: after}
|
||||
}
|
||||
|
||||
func voterChangeInfo(before []string, after []string) any {
|
||||
return struct {
|
||||
Before []string `json:"before"`
|
||||
After []string `json:"after"`
|
||||
}{Before: before, After: after}
|
||||
}
|
||||
|
||||
func userChangeInfo(before, after *User) any {
|
||||
beforeRoleNames, beforeAddresses := before.rolesAndAddresses()
|
||||
afterRoleNames, afterAddresses := after.rolesAndAddresses()
|
||||
|
||||
type userInfo struct {
|
||||
Name string `json:"name"`
|
||||
Roles []string `json:"roles"`
|
||||
Addresses []string `json:"addresses"`
|
||||
Reminder string `json:"reminder"`
|
||||
}
|
||||
details := struct {
|
||||
Before userInfo `json:"before"`
|
||||
After userInfo `json:"after"`
|
||||
}{userInfo{
|
||||
Name: before.Name,
|
||||
Roles: beforeRoleNames,
|
||||
Addresses: beforeAddresses,
|
||||
Reminder: before.Reminder.String,
|
||||
}, userInfo{
|
||||
Name: after.Name,
|
||||
Roles: afterRoleNames,
|
||||
Addresses: afterAddresses,
|
||||
Reminder: after.Reminder.String,
|
||||
}}
|
||||
|
||||
return details
|
||||
}
|
||||
|
||||
func userCreateInfo(u *User) any {
|
||||
roleNames, addresses := u.rolesAndAddresses()
|
||||
|
||||
type userInfo struct {
|
||||
Name string `json:"name"`
|
||||
Roles []string `json:"roles"`
|
||||
Addresses []string `json:"addresses"`
|
||||
Reminder string `json:"reminder"`
|
||||
}
|
||||
|
||||
details := struct {
|
||||
Created userInfo `json:"created"`
|
||||
}{Created: userInfo{
|
||||
Name: u.Name,
|
||||
Roles: roleNames,
|
||||
Addresses: addresses,
|
||||
Reminder: u.Reminder.String,
|
||||
}}
|
||||
|
||||
return details
|
||||
}
|
||||
|
|
|
@ -291,7 +291,15 @@ func (m *UserModel) Roles(ctx context.Context, user *User) ([]*Role, error) {
|
|||
return result, nil
|
||||
}
|
||||
|
||||
func (m *UserModel) Create(ctx context.Context, name string, reminder string, emails []string) (int64, error) {
|
||||
type CreateUserParams struct {
|
||||
Admin *User
|
||||
Name string
|
||||
Reminder string
|
||||
Emails []string
|
||||
Reasoning string
|
||||
}
|
||||
|
||||
func (m *UserModel) Create(ctx context.Context, params *CreateUserParams) (int64, error) {
|
||||
tx, err := m.DB.BeginTxx(ctx, nil)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("could not start transaction: %w", err)
|
||||
|
@ -304,7 +312,7 @@ func (m *UserModel) Create(ctx context.Context, name string, reminder string, em
|
|||
res, err := tx.ExecContext(
|
||||
ctx,
|
||||
`INSERT INTO voters (name, enabled) VALUES (?, 0)`,
|
||||
name)
|
||||
params.Name)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("could not insert user: %w", err)
|
||||
}
|
||||
|
@ -314,20 +322,32 @@ func (m *UserModel) Create(ctx context.Context, name string, reminder string, em
|
|||
return 0, fmt.Errorf("could not get user id: %w", err)
|
||||
}
|
||||
|
||||
for i := range emails {
|
||||
for i := range params.Emails {
|
||||
_, err := tx.ExecContext(
|
||||
ctx,
|
||||
`INSERT INTO emails (voter, address, reminder)
|
||||
VALUES (?, ?, ?)`,
|
||||
userID,
|
||||
emails[i],
|
||||
emails[i] == reminder,
|
||||
params.Emails[i],
|
||||
params.Emails[i] == params.Reminder,
|
||||
)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("could not insert email %s for voter %s: %w", emails[i], name, err)
|
||||
return 0, fmt.Errorf("could not insert email %s for voter %s: %w", params.Emails[i], params.Name, err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := AuditLog(ctx, tx, params.Admin, AuditCreateUser, params.Reasoning, userCreateInfo(&User{
|
||||
Name: params.Name,
|
||||
Reminder: sql.NullString{
|
||||
String: params.Reminder,
|
||||
Valid: true,
|
||||
},
|
||||
roles: []*Role{},
|
||||
emailAddresses: params.Emails,
|
||||
})); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
return 0, fmt.Errorf("could not commit user transaction: %w", err)
|
||||
}
|
||||
|
@ -908,37 +928,83 @@ WHERE NOT EXISTS(SELECT * FROM user_roles WHERE voter_id = ? AND role = ?)`,
|
|||
return nil
|
||||
}
|
||||
|
||||
func userChangeInfo(before, after *User) any {
|
||||
beforeRoleNames, beforeAddresses := before.rolesAndAddresses()
|
||||
afterRoleNames, afterAddresses := after.rolesAndAddresses()
|
||||
|
||||
type userInfo struct {
|
||||
Name string `json:"user"`
|
||||
Roles []string `json:"roles"`
|
||||
Addresses []string `json:"addresses"`
|
||||
Reminder string `json:"reminder"`
|
||||
func (m *UserModel) ChooseVoters(ctx context.Context, admin *User, voterIDs []int64, reasoning string) error {
|
||||
tx, err := m.DB.BeginTxx(ctx, nil)
|
||||
if err != nil {
|
||||
return errCouldNotStartTransaction(err)
|
||||
}
|
||||
details := struct {
|
||||
Before userInfo `json:"before"`
|
||||
After userInfo `json:"after"`
|
||||
}{userInfo{
|
||||
Name: before.Name,
|
||||
Roles: beforeRoleNames,
|
||||
Addresses: beforeAddresses,
|
||||
Reminder: before.Reminder.String,
|
||||
}, userInfo{
|
||||
Name: after.Name,
|
||||
Roles: afterRoleNames,
|
||||
Addresses: afterAddresses,
|
||||
Reminder: after.Reminder.String,
|
||||
}}
|
||||
|
||||
return details
|
||||
defer tx.Rollback()
|
||||
|
||||
votersBefore, err := allVoterNames(ctx, tx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
query, args, err := sqlx.In(
|
||||
`DELETE FROM user_roles WHERE voter_id NOT IN (?) AND role=?`,
|
||||
voterIDs, RoleVoter,
|
||||
)
|
||||
if err != nil {
|
||||
return errCouldNotCreateInQuery(err)
|
||||
}
|
||||
|
||||
if _, err := tx.ExecContext(ctx, query, args...); err != nil {
|
||||
return errCouldNotExecuteQuery(err)
|
||||
}
|
||||
|
||||
query, args, err = sqlx.In(`INSERT INTO user_roles (voter_id, role, created)
|
||||
SELECT v.id, ?, ?
|
||||
FROM voters v
|
||||
WHERE id IN (?) AND NOT EXISTS(SELECT * FROM user_roles WHERE role = ? AND voter_id = v.id)`,
|
||||
RoleVoter, time.Now().UTC(), voterIDs, RoleVoter)
|
||||
if err != nil {
|
||||
return errCouldNotCreateInQuery(err)
|
||||
}
|
||||
|
||||
if _, err := tx.ExecContext(ctx, query, args...); err != nil {
|
||||
return errCouldNotExecuteQuery(err)
|
||||
}
|
||||
|
||||
votersAfter, err := allVoterNames(ctx, tx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := AuditLog(ctx, tx, admin, AuditChangeVoters, reasoning, voterChangeInfo(votersBefore, votersAfter)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
return errCouldNotCommitTransaction(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func emailChangeInfo(before, after []string) any {
|
||||
return struct {
|
||||
Before []string `json:"before"`
|
||||
After []string `json:"after"`
|
||||
}{Before: before, After: after}
|
||||
func allVoterNames(ctx context.Context, tx *sqlx.Tx) ([]string, error) {
|
||||
rows, err := tx.QueryxContext(ctx, `SELECT name FROM voters JOIN user_roles ur ON voters.id = ur.voter_id WHERE role=? ORDER BY name`, RoleVoter)
|
||||
if err != nil {
|
||||
return nil, errCouldNotExecuteQuery(err)
|
||||
}
|
||||
|
||||
defer rows.Close()
|
||||
|
||||
names := make([]string, 0)
|
||||
|
||||
for rows.Next() {
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, errCouldNotFetchRow(err)
|
||||
}
|
||||
|
||||
var name string
|
||||
|
||||
if err := rows.Scan(&name); err != nil {
|
||||
return nil, errCouldNotScanResult(err)
|
||||
}
|
||||
|
||||
names = append(names, name)
|
||||
}
|
||||
|
||||
return names, nil
|
||||
}
|
||||
|
|
34
ui/html/pages/choose_voters.html
Normal file
34
ui/html/pages/choose_voters.html
Normal file
|
@ -0,0 +1,34 @@
|
|||
{{ define "title" }}Choose voters{{ end }}
|
||||
|
||||
{{ define "main" }}
|
||||
{{ $voterIDs := .Form.VoterIDs }}
|
||||
<div class="ui info message">
|
||||
<div class="header">Edit voter list</div>
|
||||
<p>Use the lists below to add or remove voters.</p>
|
||||
</div>
|
||||
<div class="ui form segment{{ if .Form.FieldErrors }} error{{ end }}">
|
||||
<form action="/voters/" method="post">
|
||||
<input type="hidden" name="csrf_token" value="{{ .CSRFToken }}">
|
||||
<label class="hidden" aria-label="voters" for="voters">Voters</label>
|
||||
<div class="field">
|
||||
<select id="voters" name="voters" class="ui fluid search dropdown" multiple aria-multiselectable="true">
|
||||
{{ range .Form.Users }}
|
||||
{{ $userID := .ID }}
|
||||
<option value="{{ .ID }}" {{ range $voterIDs }}{{ if eq $userID . }} selected{{ end }}{{ end }}>{{ .Name }}</option>
|
||||
{{ end }}
|
||||
</select>
|
||||
</div>
|
||||
<div class="required field{{ if .Form.FieldErrors.reasoning }} error{{ end }}">
|
||||
<label for="reasoning">Reasoning for the change</label>
|
||||
<textarea id="reasoning" name="reasoning" rows="2">{{ .Form.Reasoning }}</textarea>
|
||||
{{ if .Form.FieldErrors.reasoning }}
|
||||
<span class="ui small error text">{{ .Form.FieldErrors.reasoning }}</span>
|
||||
{{ end }}
|
||||
</div>
|
||||
<button class="ui primary labeled icon button" type="submit">
|
||||
<i class="edit icon"></i>
|
||||
Change voters
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
{{ end }}
|
34
ui/html/pages/create_user.html
Normal file
34
ui/html/pages/create_user.html
Normal file
|
@ -0,0 +1,34 @@
|
|||
{{ define "title" }}Add user{{ end }}
|
||||
|
||||
{{ define "main" }}
|
||||
<form action="/new-user/" method="post">
|
||||
<input type="hidden" name="csrf_token" , value="{{ .CSRFToken }}">
|
||||
<div class="ui form segment{{ if .Form.FieldErrors }} error{{ end }}">
|
||||
<div class="required field{{ if .Form.FieldErrors.name }} error{{ end }}">
|
||||
<label for="name">Name</label>
|
||||
<input id="name" type="text" name="name" value="{{ .Form.Name }}">
|
||||
{{ if .Form.FieldErrors.name }}
|
||||
<span class="ui small error text">{{ .Form.FieldErrors.name }}</span>
|
||||
{{ end }}
|
||||
</div>
|
||||
<div class="required field{{ if .Form.FieldErrors.email_address }} error{{ end}}">
|
||||
<label for="email_address">Email address</label>
|
||||
<input id="email_address" name="email_address" type="email" value="{{ .Form.EmailAddress }}">
|
||||
{{ if .Form.FieldErrors.email_address }}
|
||||
<span class="ui small error text">{{ .Form.FieldErrors.email_address }}</span>
|
||||
{{ end }}
|
||||
</div>
|
||||
<div class="required field{{ if .Form.FieldErrors.reasoning }} error{{ end }}">
|
||||
<label for="reasoning">Reasoning for the change</label>
|
||||
<textarea id="reasoning" name="reasoning" rows="2">{{ .Form.Reasoning }}</textarea>
|
||||
{{ if .Form.FieldErrors.reasoning }}
|
||||
<span class="ui small error text">{{ .Form.FieldErrors.reasoning }}</span>
|
||||
{{ end }}
|
||||
</div>
|
||||
<button class="ui primary labeled icon button" type="submit">
|
||||
<i class="magic icon"></i> Add user
|
||||
</button>
|
||||
<a href="/users/" class="ui button">Cancel</a>
|
||||
</div>
|
||||
</form>
|
||||
{{ end }}
|
|
@ -70,7 +70,7 @@
|
|||
{{ end }}
|
||||
</div>
|
||||
<button class="ui primary labeled icon button" type="submit">
|
||||
<i class="trash icon"></i> Edit user
|
||||
<i class="edit icon"></i> Edit user
|
||||
</button>
|
||||
<a href="/users/" class="ui button">Cancel</a>
|
||||
</div>
|
||||
|
|
|
@ -33,6 +33,11 @@
|
|||
{{ if canManageUsers $user }}
|
||||
<nav class="ui secondary pointing menu">
|
||||
<a href="/users/" class="{{ if eq .ActiveSubNav "users" }}active {{ end }}item">Manage users</a>
|
||||
<a href="/voters/" class="{{ if eq .ActiveSubNav "manage-voters" }}active {{ end }}item">Manage voters</a>
|
||||
<div class="right item">
|
||||
<a class="ui primary labeled icon button" href="/new-user/">
|
||||
<i class="magic icon"></i> Add User</a>
|
||||
</div>
|
||||
</nav>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
|
|
|
@ -2,4 +2,6 @@ $(document).ready(function () {
|
|||
$('.message .close').on('click', function () {
|
||||
$(this).closest('.message').transition('fade');
|
||||
});
|
||||
|
||||
$('.ui.dropdown').dropdown();
|
||||
});
|
||||
|
|
Loading…
Reference in a new issue