diff --git a/cmd/web/handlers.go b/cmd/web/handlers.go index bb3f645..911250e 100644 --- a/cmd/web/handlers.go +++ b/cmd/web/handlers.go @@ -3,6 +3,8 @@ package main import ( api "code.dlmw.ch/dlmw/qv/internal" "code.dlmw.ch/dlmw/qv/internal/validator" + "encoding/json" + "math/rand" "net/http" "time" ) @@ -26,7 +28,7 @@ func (app *application) createElection(w http.ResponseWriter, r *http.Request) { request.CheckField(validator.GreaterThan(len(request.Choices), 1), "choices", "there must be more than 1 choice") request.CheckField( - !(request.AreVotersKnown && (request.MaxVoters == nil || *request.MaxVoters < 1)), + !(request.AreVotersKnown && request.MaxVoters < 1), "maxVoters", "must be greater than 0 when voters are known", ) @@ -36,13 +38,39 @@ func (app *application) createElection(w http.ResponseWriter, r *http.Request) { return } - _, err = app.elections.Insert(request.Name, request.Tokens, request.AreVotersKnown, request.MaxVoters, request.Choices, request.ExpiresAt) + 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 } - //TODO: if voters are known, generate voters and write them in the response (change openapi as well) + var res []byte + if request.AreVotersKnown { + voterIdentities := make([]string, 0, request.MaxVoters) + for i := 0; i < request.MaxVoters; i++ { + randomIdentity := generateRandomVoterIdentity() + _, err := app.voters.Insert(randomIdentity, electionId) + if err != nil { + app.serverError(w, r, err) + } + voterIdentities = append(voterIdentities, randomIdentity) + } - w.Write([]byte("TODO")) + res, err = json.Marshal(api.CreateElectionResponse{VoterIdentities: &voterIdentities}) + if err != nil { + app.serverError(w, r, err) + return + } + } + + w.Write(res) +} + +func generateRandomVoterIdentity() string { + const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + b := make([]byte, 16) + for i := range b { + b[i] = charset[rand.Intn(len(charset))] + } + return string(b) } diff --git a/cmd/web/main.go b/cmd/web/main.go index 2d5b771..a465e34 100644 --- a/cmd/web/main.go +++ b/cmd/web/main.go @@ -19,6 +19,7 @@ import ( type application struct { logger *slog.Logger elections models.ElectionModelInterface + voters models.VoterModelInterface } var addr = ":8080" @@ -41,6 +42,7 @@ func main() { app := &application{ logger: logger, elections: &models.ElectionModel{DB: db}, + voters: &models.VoterModel{DB: db}, } logger.Info("Starting server", "addr", addr) diff --git a/cmd/web/openapi.yml b/cmd/web/openapi.yml index f2ab383..9e4fd91 100644 --- a/cmd/web/openapi.yml +++ b/cmd/web/openapi.yml @@ -32,8 +32,12 @@ paths: schema: $ref: "#/components/schemas/CreateElectionRequest" responses: - 201: - description: Election created + 200: + description: Election created. Body only returned if voterAreKnown is true + content: + application/json: + schema: + $ref: "#/components/schemas/CreateElectionResponse" 422: description: Unprocessable Content content: @@ -49,8 +53,9 @@ components: - id - name - tokens - - are_voters_known - - expires_at + - areVotersKnown + - maxVoters + - expiresAt properties: id: type: integer @@ -60,20 +65,19 @@ components: type: string minLength: 1 tokens: - type: integer - minimum: 0 - are_voters_known: - type: boolean - max_voters: type: integer minimum: 1 - nullable: true - description: Required when voters are known - created_at: + areVotersKnown: + type: boolean + maxVoters: + type: integer + minimum: 0 + description: Must be greater than 0 when voters are known + createdAt: type: string format: date-time readOnly: true - expires_at: + expiresAt: type: string format: date-time choices: @@ -89,12 +93,12 @@ components: type: object required: - text - - election_id + - electionId properties: text: type: string minLength: 1 - election_id: + electionId: type: integer format: int64 @@ -102,13 +106,13 @@ components: type: object required: - identity - - election_id + - electionId properties: identity: type: string minLength: 1 description: When voters are known, passcodes will be pre-generated - election_id: + electionId: type: integer format: int64 votes: @@ -119,27 +123,27 @@ components: Vote: type: object required: - - voter_identity - - election_id + - voterIdentity + - electionId properties: - voter_identity: + voterIdentity: type: string minLength: 1 - election_id: + electionId: type: integer format: int64 - choice_text: + choiceText: type: string nullable: true tokens: type: integer - minimum: 0 + minimum: 1 nullable: true - calculated_vote_count: + calculatedVoteCount: type: integer readOnly: true description: Calculated as floor(sqrt(tokens)) - created_at: + createdAt: type: string format: date-time readOnly: true @@ -149,24 +153,24 @@ components: required: - name - tokens - - are_voters_known - - expires_at + - areVotersKnown + - maxVoters + - expiresAt - choices properties: name: type: string minLength: 1 tokens: - type: integer - minimum: 0 - are_voters_known: - type: boolean - max_voters: type: integer minimum: 1 - nullable: true - description: Required when voters are known - expires_at: + areVotersKnown: + type: boolean + maxVoters: + type: integer + minimum: 0 + description: Must be greater than 0 when voters are known; 0 = no limit + expiresAt: type: string format: date-time choices: @@ -177,6 +181,17 @@ components: minItems: 1 uniqueItems: true + CreateElectionResponse: + type: object + properties: + voterIdentities: + type: array + items: + type: string + minLength: 1 + maxItems: 1 + uniqueItems: true + ErrorResponse: type: object required: diff --git a/internal/generated.go b/internal/generated.go index a772d92..5add100 100644 --- a/internal/generated.go +++ b/internal/generated.go @@ -13,16 +13,21 @@ import ( // CreateElectionRequest defines model for CreateElectionRequest. type CreateElectionRequest struct { - AreVotersKnown bool `json:"are_voters_known"` + AreVotersKnown bool `json:"areVotersKnown"` Choices []string `json:"choices"` - ExpiresAt time.Time `json:"expires_at"` + ExpiresAt time.Time `json:"expiresAt"` - // MaxVoters Required when voters are known - MaxVoters *int `json:"max_voters"` + // MaxVoters Must be greater than 0 when voters are known; 0 = no limit + MaxVoters int `json:"maxVoters"` Name string `json:"name"` Tokens int `json:"tokens"` } +// CreateElectionResponse defines model for CreateElectionResponse. +type CreateElectionResponse struct { + VoterIdentities *[]string `json:"voterIdentities,omitempty"` +} + // ErrorResponse defines model for ErrorResponse. type ErrorResponse struct { // Code Machine-readable error code diff --git a/internal/models/elections.go b/internal/models/elections.go index 19463a5..e410a1f 100644 --- a/internal/models/elections.go +++ b/internal/models/elections.go @@ -2,22 +2,21 @@ package models import ( "database/sql" - "fmt" "time" ) type ElectionModelInterface interface { - Insert(name string, tokens int, areVotersKnown bool, maxVoters *int, Choices []string, ExpiresAt time.Time) (int, error) + Insert(name string, tokens int, areVotersKnown bool, maxVoters int, Choices []string, ExpiresAt time.Time) (int, error) } type ElectionModel struct { DB *sql.DB } -func (e *ElectionModel) Insert(name string, tokens int, areVotersKnown bool, maxVoters *int, choices []string, expiresAt time.Time) (int, error) { +func (e *ElectionModel) Insert(name string, tokens int, areVotersKnown bool, maxVoters int, choices []string, expiresAt time.Time) (int, error) { tx, err := e.DB.Begin() if err != nil { - return 0, fmt.Errorf("begin transaction: %w", err) + return 0, err } defer tx.Rollback() diff --git a/internal/models/voters.go b/internal/models/voters.go new file mode 100644 index 0000000..4545ad8 --- /dev/null +++ b/internal/models/voters.go @@ -0,0 +1,36 @@ +package models + +import ( + "database/sql" +) + +type VoterModelInterface interface { + Insert(identity string, electionID int) (int, error) +} + +type VoterModel struct { + DB *sql.DB +} + +func (v *VoterModel) Insert(identity string, electionID int) (int, error) { + tx, err := v.DB.Begin() + if err != nil { + return 0, err + } + defer tx.Rollback() + + result, err := tx.Exec(` + INSERT INTO voters (identity, election_id) + VALUES (?, ?)`, + identity, electionID) + if err != nil { + return 0, err + } + + if err = tx.Commit(); err != nil { + return 0, err + } + + voterId, err := result.LastInsertId() + return int(voterId), nil +}