276 lines
7.3 KiB
Go
276 lines
7.3 KiB
Go
package main
|
|
|
|
import (
|
|
api "code.dlmw.ch/dlmw/qv/internal"
|
|
"code.dlmw.ch/dlmw/qv/internal/models"
|
|
"code.dlmw.ch/dlmw/qv/internal/validator"
|
|
"code.dlmw.ch/dlmw/qv/ui"
|
|
"database/sql"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"math/rand"
|
|
"net/http"
|
|
"slices"
|
|
"time"
|
|
)
|
|
|
|
func (app *application) createElectionPage(w http.ResponseWriter, r *http.Request) {
|
|
content, err := ui.Files.ReadFile("create-election.html")
|
|
if err != nil {
|
|
app.serverError(w, r, fmt.Errorf("couldn't read create-election.html"))
|
|
return
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "text/html")
|
|
w.Write(content)
|
|
}
|
|
|
|
type createElectionRequestWithValidator struct {
|
|
api.CreateElectionRequest
|
|
validator.Validator
|
|
}
|
|
|
|
func (r *createElectionRequestWithValidator) isValid() bool {
|
|
r.CheckField(validator.NotBlank(r.Name), "name", "must not be blank")
|
|
r.CheckField(validator.GreaterThan(r.Tokens, 0), "tokens", "must be greater than 0")
|
|
r.CheckField(validator.After(r.ExpiresAt, time.Now()), "expiresAt", "must expire in a future date")
|
|
r.CheckField(validator.GreaterThan(len(r.Choices), 1), "choices", "there must be more than 1 choice")
|
|
r.CheckField(validator.UniqueValues(r.Choices), "choices", "must not contain duplicate values")
|
|
|
|
for _, choice := range r.Choices {
|
|
r.CheckField(validator.NotBlank(choice), "choice", "must not be blank")
|
|
}
|
|
|
|
if r.AreVotersKnown {
|
|
r.CheckField(
|
|
validator.GreaterThan(r.MaxVoters, 0),
|
|
"maxVoters",
|
|
"must be greater than 0 when voters are known",
|
|
)
|
|
} else {
|
|
r.CheckField(
|
|
validator.GreaterThanOrEquals(r.MaxVoters, 0),
|
|
"maxVoters",
|
|
"must be a positive number",
|
|
)
|
|
}
|
|
|
|
return r.Valid()
|
|
}
|
|
|
|
func (app *application) CreateElection(w http.ResponseWriter, r *http.Request) {
|
|
var request createElectionRequestWithValidator
|
|
|
|
if err := app.unmarshalRequest(r, &request); err != nil {
|
|
app.clientError(w, http.StatusBadRequest, err.Error())
|
|
return
|
|
}
|
|
|
|
if !request.isValid() {
|
|
app.unprocessableEntityError(w, request.Validator)
|
|
return
|
|
}
|
|
|
|
electionId, err := app.elections.Insert(
|
|
request.Name,
|
|
request.Tokens,
|
|
request.AreVotersKnown,
|
|
request.MaxVoters,
|
|
request.Choices,
|
|
request.ExpiresAt,
|
|
)
|
|
if err != nil {
|
|
app.serverError(w, r, err)
|
|
return
|
|
}
|
|
|
|
var res []byte
|
|
if request.AreVotersKnown {
|
|
voterIdentities := make([]string, 0, request.MaxVoters)
|
|
for i := 0; i < request.MaxVoters; i++ {
|
|
randomIdentity := randomVoterIdentity()
|
|
voterIdentities = append(voterIdentities, randomIdentity)
|
|
}
|
|
_, err = app.voters.InsertMultiple(voterIdentities, electionId)
|
|
if err != nil {
|
|
app.serverError(w, r, err)
|
|
return
|
|
}
|
|
|
|
res, err = json.Marshal(api.CreateElectionResponse{VoterIdentities: &voterIdentities})
|
|
if err != nil {
|
|
app.serverError(w, r, err)
|
|
return
|
|
}
|
|
}
|
|
|
|
w.Header().Set("Location", fmt.Sprintf("/election/%v", electionId))
|
|
w.Write(res)
|
|
}
|
|
|
|
func randomVoterIdentity() string {
|
|
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
|
b := make([]byte, 16)
|
|
for i := range b {
|
|
b[i] = charset[rand.Intn(len(charset))]
|
|
}
|
|
return string(b)
|
|
}
|
|
|
|
type createVotesRequestWithValidator struct {
|
|
api.CreateVotesRequest
|
|
validator.Validator
|
|
}
|
|
|
|
func (r *createVotesRequestWithValidator) isValid() bool {
|
|
for _, choice := range r.Choices {
|
|
r.CheckField(validator.NotBlank(choice.ChoiceText), "choiceText", "must not be blank")
|
|
}
|
|
|
|
return r.Valid()
|
|
}
|
|
|
|
func (app *application) CreateVotes(w http.ResponseWriter, r *http.Request, id int) {
|
|
var request createVotesRequestWithValidator
|
|
|
|
if err := app.unmarshalRequest(r, &request); err != nil {
|
|
app.clientError(w, http.StatusBadRequest, err.Error())
|
|
return
|
|
}
|
|
|
|
if !request.isValid() {
|
|
app.unprocessableEntityError(w, request.Validator)
|
|
return
|
|
}
|
|
|
|
election, err := app.elections.GetById(id)
|
|
if err != nil {
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
app.clientError(w, http.StatusNotFound, "Couldn't find an election with the ID you provided")
|
|
return
|
|
}
|
|
app.serverError(w, r, err)
|
|
}
|
|
|
|
for _, c := range request.Choices {
|
|
choiceExists := slices.Contains(election.Choices, c.ChoiceText)
|
|
if !choiceExists {
|
|
app.clientError(w, http.StatusUnprocessableEntity, fmt.Sprintf("choice %v doesn't exist", c.ChoiceText))
|
|
return
|
|
}
|
|
}
|
|
|
|
tokensUsed := 0
|
|
for _, c := range request.Choices {
|
|
tokensUsed += c.Tokens
|
|
}
|
|
|
|
if tokensUsed > election.Tokens {
|
|
app.clientError(w, http.StatusUnprocessableEntity, fmt.Sprintf("you used too many tokens; must not exceed %v tokens", election.Tokens))
|
|
return
|
|
}
|
|
|
|
electionHasExpired := election.ExpiresAt.Before(time.Now())
|
|
if electionHasExpired {
|
|
app.clientError(w, http.StatusUnprocessableEntity, "election has expired")
|
|
return
|
|
}
|
|
|
|
var voterIdentity string
|
|
if election.AreVotersKnown {
|
|
voterIdentity, err = app.createVotesHandleKnownVotersElection(w, r, request, election)
|
|
if err != nil {
|
|
// everything will have been logged already
|
|
return
|
|
}
|
|
} else {
|
|
voterIdentity, err = app.createVotesHandleUnknownVotersElection(w, r, election)
|
|
if err != nil {
|
|
// everything will have been logged already
|
|
return
|
|
}
|
|
}
|
|
|
|
for _, c := range request.Choices {
|
|
_, err := app.votes.Insert(voterIdentity, election.ID, c.ChoiceText, c.Tokens)
|
|
if err != nil {
|
|
app.serverError(w, r, err)
|
|
return
|
|
}
|
|
}
|
|
|
|
w.WriteHeader(http.StatusCreated)
|
|
}
|
|
|
|
func (app *application) createVotesHandleKnownVotersElection(w http.ResponseWriter, r *http.Request, request createVotesRequestWithValidator, election *models.Election) (string, error) {
|
|
if request.VoterIdentity == nil || validator.Blank(*request.VoterIdentity) {
|
|
message := "election has known voters; you must provide an identity provided by the organizer"
|
|
app.clientError(w, http.StatusUnprocessableEntity, message)
|
|
return "", fmt.Errorf(message)
|
|
}
|
|
|
|
voterIdentity := *request.VoterIdentity
|
|
hasCastVotes, err := app.votes.Exists(voterIdentity, election.ID)
|
|
if err != nil {
|
|
app.serverError(w, r, err)
|
|
return "", err
|
|
}
|
|
if hasCastVotes {
|
|
message := "you already voted"
|
|
app.clientError(w, http.StatusUnprocessableEntity, message)
|
|
return "", fmt.Errorf(message)
|
|
}
|
|
|
|
voterExists, err := app.voters.Exists(voterIdentity, election.ID)
|
|
if err != nil {
|
|
app.serverError(w, r, err)
|
|
return "", err
|
|
}
|
|
if !voterExists {
|
|
message := "invalid voter identity"
|
|
app.clientError(w, http.StatusUnprocessableEntity, message)
|
|
return "", fmt.Errorf(message)
|
|
}
|
|
|
|
return voterIdentity, nil
|
|
}
|
|
|
|
func (app *application) createVotesHandleUnknownVotersElection(w http.ResponseWriter, r *http.Request, election *models.Election) (string, error) {
|
|
voterIdentity := r.RemoteAddr
|
|
|
|
voterExists, err := app.voters.Exists(voterIdentity, election.ID)
|
|
if err != nil {
|
|
app.serverError(w, r, err)
|
|
return "", err
|
|
}
|
|
if voterExists {
|
|
message := "you already voted"
|
|
app.clientError(w, http.StatusUnprocessableEntity, message)
|
|
return "", fmt.Errorf(message)
|
|
}
|
|
|
|
_, err = app.voters.InsertMultiple([]string{voterIdentity}, election.ID)
|
|
if err != nil {
|
|
app.serverError(w, r, err)
|
|
return "", err
|
|
}
|
|
|
|
voterCount, err := app.voters.CountByElection(election.ID)
|
|
if err != nil && !errors.Is(sql.ErrNoRows, err) {
|
|
app.serverError(w, r, err)
|
|
return "", err
|
|
}
|
|
if voterCount == election.MaxVoters {
|
|
message := "maximum voters reached"
|
|
app.clientError(w, http.StatusUnprocessableEntity, message)
|
|
return "", fmt.Errorf(message)
|
|
}
|
|
|
|
return voterIdentity, nil
|
|
}
|
|
|
|
func (app *application) GetElectionResults(w http.ResponseWriter, r *http.Request, id int) {
|
|
|
|
}
|