package main import ( api "code.dlmw.ch/dlmw/qv/internal" "code.dlmw.ch/dlmw/qv/internal/mappers" "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" "math/rand" "net/http" "slices" "time" ) func (app *application) indexPage(w http.ResponseWriter, r *http.Request) { content, err := ui.Files.ReadFile("index.html") if err != nil { app.serverError(w, r, err) return } w.Header().Set("Content-Type", "text/html") w.Write(content) } 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, err) return } w.Header().Set("Content-Type", "text/html") w.Write(content) } func (app *application) getElectionResultsPage(w http.ResponseWriter, r *http.Request) { content, err := ui.Files.ReadFile("election-results.html") if err != nil { app.serverError(w, r, err) return } w.Header().Set("Content-Type", "text/html") w.Write(content) } func (app *application) getElectionPage(w http.ResponseWriter, r *http.Request) { content, err := ui.Files.ReadFile("election.html") if err != nil { app.serverError(w, r, err) 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") r.CheckField(validator.LesserThan(r.MaxVoters, 101), "maxVoters", "cannot create a known-voters election with more than 100 voters") 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("Content-Type", "application/json") } 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 string) { 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) } voterCount, err := app.voters.CountByElection(election.ID) if err != nil && !errors.Is(sql.ErrNoRows, err) { app.serverError(w, r, err) return "", err } if voterCount != 0 && voterCount == election.MaxVoters { message := "maximum voters reached" 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 } return voterIdentity, nil } func (app *application) GetElectionResults(w http.ResponseWriter, r *http.Request, id string) { votes, err := app.votes.GetByElection(id) if err != nil { if errors.Is(sql.ErrNoRows, err) { app.clientError(w, http.StatusNotFound, fmt.Sprintf("couldn't find votes for an election with id %v", id)) return } app.serverError(w, r, err) return } results := getResultsFromVotes(votes) response, err := json.Marshal(api.ElectionResultsResponse{Results: &results}) if err != nil { app.serverError(w, r, err) return } w.Header().Set("Content-Type", "application/json") w.Write(response) } func getResultsFromVotes(votes *[]models.Vote) []api.VotesForChoice { votesForChoice := make(map[string]int) for _, v := range *votes { votesForChoice[v.ChoiceText] += int(math.Floor(math.Sqrt(float64(v.Tokens)))) } results := make([]api.VotesForChoice, 0) for choice, votes := range votesForChoice { result := api.VotesForChoice{ Choice: choice, Votes: votes, } results = append(results, result) } return results } func (app *application) GetElection(w http.ResponseWriter, r *http.Request, id string) { election, err := app.elections.GetById(id) if err != nil { if errors.Is(sql.ErrNoRows, err) { app.clientError(w, http.StatusNotFound, fmt.Sprintf("couldn't find election with id %v", id)) return } app.serverError(w, r, err) return } response, err := json.Marshal(mappers.ElectionResponse(election)) if err != nil { app.serverError(w, r, err) return } w.Header().Set("Content-Type", "application/json") w.Write(response) }