/* 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" "net/http" "strings" "time" "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) } templateData := app.newTemplateData(r.Context(), r, "motions", "all-motions") if listOptions.UnvotedOnly { templateData.ActiveSubNav = subLevelNavMotionsUnvoted } templateData.Motions = motions err = templateData.motionPaginationOptions(listOptions.Limit, first, last) if err != nil { panic(err) } 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 } func (app *application) motionDetails(w http.ResponseWriter, r *http.Request) { motion := app.motionFromRequestParam(r.Context(), w, r) if motion == nil { return } 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) { data := app.newTemplateData(r.Context(), r, topLevelNavMotions, subLevelNavMotionsAll) data.Form = &forms.EditMotionForm{ Type: models.VoteTypeMotion, } app.render(w, http.StatusOK, "create_motion.html", data) } const hoursInDay = 24 func (app *application) newMotionSubmit(w http.ResponseWriter, r *http.Request) { 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() { 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{ Decision: decision, Proposer: user, }) app.jobScheduler.Reschedule(JobIDCloseDecisions, JobIDRemindVoters) app.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 (app *application) editMotionForm(w http.ResponseWriter, r *http.Request) { motion := app.motionFromRequestParam(r.Context(), w, r) if motion == nil { return } data := app.newTemplateData(r.Context(), r, topLevelNavMotions, subLevelNavMotionsAll) 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) } func (app *application) editMotionSubmit(w http.ResponseWriter, r *http.Request) { motion := app.motionFromRequestParam(r.Context(), w, r) if motion == nil { return } 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() { data := app.newTemplateData(r.Context(), r, topLevelNavMotions, subLevelNavMotionsAll) data.Form = &form app.render(w, http.StatusUnprocessableEntity, "edit_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 err = app.motions.Update( r.Context(), motion.ID, 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) }, ) if err != nil { panic(err) } decision, err := app.motions.ByID(r.Context(), motion.ID) if err != nil { panic(err) } app.mailNotifier.Notify(&UpdateDecisionNotification{ Decision: decision, User: user, }) app.jobScheduler.Reschedule(JobIDCloseDecisions, JobIDRemindVoters) app.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 (app *application) withdrawMotionForm(w http.ResponseWriter, r *http.Request) { motion := app.motionFromRequestParam(r.Context(), w, r) if motion == nil { app.notFound(w) return } data := app.newTemplateData(r.Context(), r, topLevelNavMotions, subLevelNavMotionsAll) data.Motion = motion app.render(w, http.StatusOK, "withdraw_motion.html", data) } func (app *application) withdrawMotionSubmit(w http.ResponseWriter, r *http.Request) { motion := app.motionFromRequestParam(r.Context(), w, r) 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) if err != nil { panic(err) } app.mailNotifier.Notify(&WithDrawMotionNotification{motion, user}) app.jobScheduler.Reschedule(JobIDCloseDecisions, JobIDRemindVoters) app.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 (app *application) voteForm(w http.ResponseWriter, r *http.Request) { motion := app.motionFromRequestParam(r.Context(), w, r) if motion == nil { app.notFound(w) return } choice := app.choiceFromRequestParam(w, r) if choice == nil { app.notFound(w) return } data := app.newTemplateData(r.Context(), r, topLevelNavMotions, subLevelNavMotionsAll) data.Motion = motion data.Form = &forms.DirectVoteForm{ Choice: choice, } app.render(w, http.StatusOK, "direct_vote.html", data) } func (app *application) voteSubmit(w http.ResponseWriter, r *http.Request) { motion := app.motionFromRequestParam(r.Context(), w, r) if motion == nil { return } choice := app.choiceFromRequestParam(w, r) 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) } if err := app.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) } app.mailNotifier.Notify(&DirectVoteNotification{ Decision: motion, User: user, Choice: choice, }) app.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 (app *application) proxyVoteForm(w http.ResponseWriter, r *http.Request) { motion := app.motionFromRequestParam(r.Context(), w, r) if motion == nil { return } data := app.newTemplateData(r.Context(), r, topLevelNavMotions, subLevelNavMotionsAll) data.Motion = motion potentialVoters, err := app.users.Voters(r.Context()) if err != nil { panic(err) } data.Form = &forms.ProxyVoteForm{ Voters: potentialVoters, } app.render(w, http.StatusOK, "proxy_vote.html", data) } func (app *application) proxyVoteSubmit(w http.ResponseWriter, r *http.Request) { motion := app.motionFromRequestParam(r.Context(), w, r) 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) } if !form.Valid() { data := app.newTemplateData(r.Context(), r, topLevelNavMotions, subLevelNavMotionsAll) data.Motion = motion potentialVoters, err := app.users.Voters(r.Context()) if err != nil { panic(err) } form.Voters = potentialVoters data.Form = &form 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) } voter := app.getVoter(r.Context(), w, form.Voter.ID) if voter == nil { return } if err := app.motions.UpdateVote(r.Context(), form.Voter.ID, motion.ID, func(v *models.Vote) { 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) } app.mailNotifier.Notify(&ProxyVoteNotification{ Decision: motion, User: user, Voter: voter, Choice: form.Choice, Justification: form.Justification, }) app.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 (app *application) userList(w http.ResponseWriter, r *http.Request) { data := app.newTemplateData(r.Context(), r, topLevelNavUsers, subLevelNavUsers) users, err := app.users.List( r.Context(), app.users.WithRoles(), app.users.CanDelete(), ) if err != nil { panic(err) } data.Users = users app.render(w, http.StatusOK, "users.html", data) } func (app *application) editUserForm(w http.ResponseWriter, r *http.Request) { userToEdit := app.userFromRequestParam(r.Context(), w, r, app.users.WithRoles(), app.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 := app.newTemplateData(r.Context(), r, topLevelNavUsers, subLevelNavUsers) 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) } func (app *application) editUserSubmit(w http.ResponseWriter, r *http.Request) { userToEdit := app.userFromRequestParam(r.Context(), w, r, app.users.WithRoles(), app.users.WithEmailAddresses()) if userToEdit == nil { app.notFound(w) return } var form forms.EditUserForm if err := app.decodePostForm(r, &form); err != nil { app.clientError(w, http.StatusBadRequest) return } form.User = userToEdit form.AllRoles = models.AllRoles if err := form.Validate(); err != nil { panic(err) } data := app.newTemplateData(r.Context(), r, topLevelNavUsers, subLevelNavUsers) if !form.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 } form.MailAddresses = emailAddresses form.Roles = roleNames data.Form = &form app.render(w, http.StatusUnprocessableEntity, "edit_user.html", data) return } form.Normalize() form.UpdateUser(userToEdit) if err := app.users.EditUser(r.Context(), userToEdit, data.User, form.Roles, form.Reasoning); err != nil { panic(err) } app.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 (app *application) userAddEmailForm(w http.ResponseWriter, r *http.Request) { userToEdit := app.userFromRequestParam(r.Context(), w, r, app.users.WithEmailAddresses()) if userToEdit == nil { app.notFound(w) return } emailAddresses, err := userToEdit.EmailAddresses() if err != nil { panic(err) } data := app.newTemplateData(r.Context(), r, topLevelNavUsers, subLevelNavUsers) data.Form = &forms.AddEmailForm{ User: userToEdit, EmailAddresses: emailAddresses, } app.render(w, http.StatusOK, "add_email.html", data) } func (app *application) userAddEmailSubmit(w http.ResponseWriter, r *http.Request) { userToEdit := app.userFromRequestParam(r.Context(), w, r, app.users.WithEmailAddresses()) 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) } data := app.newTemplateData(r.Context(), r, topLevelNavUsers, subLevelNavUsers) if form.Valid() { emailExists, err := app.users.EmailExists(r.Context(), form.EmailAddress) if err != nil { panic(err) } 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) } 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) } app.addFlash(r.Context(), &FlashMessage{ Variant: flashSuccess, Title: "Email address added", Message: fmt.Sprintf( "Added email address %s for user %s", form.EmailAddress, userToEdit.Name, ), }) http.Redirect(w, r, fmt.Sprintf("/users/%d/", userToEdit.ID), http.StatusSeeOther) } func (app *application) userDeleteEmailForm(w http.ResponseWriter, r *http.Request) { userToEdit, emailAddress, err := app.deleteEmailParams(r.Context(), w, r) if err != nil { panic(err) } 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 } data := app.newTemplateData(r.Context(), r, topLevelNavUsers, subLevelNavUsers) data.Form = &forms.DeleteEmailForm{ User: userToEdit, EmailAddress: emailAddress, } app.render(w, http.StatusOK, "delete_email.html", data) } func (app *application) userDeleteEmailSubmit(w http.ResponseWriter, r *http.Request) { userToEdit, emailAddress, err := app.deleteEmailParams(r.Context(), w, r) if err != nil { panic(err) } 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) } if !form.Valid() { form.EmailAddress = emailAddress form.User = userToEdit data := app.newTemplateData(r.Context(), r, topLevelNavUsers, subLevelNavUsers) data.Form = &form 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) } app.addFlash(r.Context(), &FlashMessage{ 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) } func (app *application) newUserForm(w http.ResponseWriter, r *http.Request) { data := app.newTemplateData(r.Context(), r, topLevelNavUsers, subLevelNavUsers) data.Form = &forms.NewUserForm{} app.render(w, http.StatusOK, "create_user.html", data) } 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.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) } 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) } func (app *application) deleteUserForm(w http.ResponseWriter, r *http.Request) { userToDelete := app.userFromRequestParam(r.Context(), w, r, app.users.CanDelete()) if userToDelete == nil { return } if !userToDelete.CanDelete() { app.notFound(w) return } 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) { 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) } 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) } app.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 (app *application) chooseVotersForm(w http.ResponseWriter, r *http.Request) { 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() { 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) } 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") 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) }