Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@ func TestNominationSubmit(t *testing.T) {
"zid": zid,
"otp": code,
})
if resp.Code != 204 {
t.Fatalf("expected 204 OK, got %d", resp.Code)
if resp.Code != 200 {
t.Fatalf("expected 200 OK, got %d", resp.Code)
}

res := resp.Result()
Expand Down
23 changes: 21 additions & 2 deletions backend/internal/api/v1/handlers/handlers_test/otp_test.go
Original file line number Diff line number Diff line change
@@ -1,23 +1,42 @@
package handlers_test

import (
"encoding/json"
"testing"
"time"

"github.com/linuxunsw/vote/backend/internal/api/v1/models"
"github.com/linuxunsw/vote/backend/internal/config"
)

func TestOTPConsumeMember(t *testing.T) {
cfg := config.Load()
api, mailer := NewAPI(t)

then := time.Now().Add(-time.Second) // one second before
zid := "z0000000"
_ = createElection(t, api, cfg.JWT, TestingDummyJWTAdmin, []string{
zid,
})

resp := generateOTPSubmit(t, api, mailer, zid)
if resp.Code != 204 {
t.Fatalf("expected 204 OK, got %d", resp.Code)
if resp.Code != 200 {
t.Fatalf("expected 200 OK, got %d", resp.Code)
}
body := models.SubmitOTPResponseBody{};
err := json.Unmarshal(resp.Body.Bytes(), &body)
if err != nil {
t.Fatalf("expected body, got error %v", err)
}

if body.Zid != zid {
t.Fatalf("expected zid")
}
if body.IsAdmin {
t.Fatalf("expected not to be admin")
}
if body.Expiry.Before(then.Add(cfg.JWT.Duration)) {
t.Fatalf("expected expiry duration within 1 second")
}

res := resp.Result()
Expand Down
17 changes: 16 additions & 1 deletion backend/internal/api/v1/handlers/otp.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"log/slog"
"net/http"
"strconv"
"time"

"github.com/danielgtaylor/huma/v2"
"github.com/lestrrat-go/jwx/v3/jwa"
Expand Down Expand Up @@ -134,9 +135,16 @@ func SubmitOTP(log *slog.Logger, st store.OTPStore, el store.ElectionStore, cfg
log.Info("admin login", "zid", input.Body.Zid, "request_id", requestid.Get(ctx))
}

tokenExpiry := time.Now().Add(cfg.Duration)

// user is now authenticated and authorised as a society member
// create JWT
token, err := jwt.NewBuilder().Issuer(cfg.Issuer).Subject(input.Body.Zid).Claim("isAdmin", isAdmin).Build()
token, err := jwt.NewBuilder().
Issuer(cfg.Issuer).
Subject(input.Body.Zid).
Claim("isAdmin", isAdmin).
Expiration(tokenExpiry).
Build()
if err != nil {
log.Error("failed to build JWT", "error", err, "request_id", requestid.Get(ctx))
return nil, huma.Error500InternalServerError("internal error")
Expand All @@ -161,6 +169,13 @@ func SubmitOTP(log *slog.Logger, st store.OTPStore, el store.ElectionStore, cfg
Secure: true,
SameSite: http.SameSiteLaxMode,
Path: "/",
Expires: tokenExpiry,
},

Body: models.SubmitOTPResponseBody{
Zid: input.Body.Zid,
Expiry: tokenExpiry,
IsAdmin: isAdmin,
},
}, nil
}
Expand Down
6 changes: 4 additions & 2 deletions backend/internal/api/v1/models/election.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package models

import "github.com/linuxunsw/vote/backend/internal/store"
import (
"github.com/linuxunsw/vote/backend/internal/store"
)

type CreateElectionInput struct {
Body struct {
Expand Down Expand Up @@ -38,7 +40,7 @@ type GetElectionStateResponse struct {
type GetElectionStateResponseBody struct {
State string `json:"state" enum:"NO_ELECTION,CLOSED,NOMINATIONS_OPEN,NOMINATIONS_CLOSED,VOTING_OPEN,VOTING_CLOSED,RESULTS,END"`
ElectionId string `json:"election_id,omitempty" doc:"Election ID. Only set if an election is running."`
StateCreatedAt string `json:"state_created_at,omitempty" doc:"Timestamp when the election first entered this state, in RFC3339 format. Only Set if an election is running." example:"2023-01-01T00:00:00Z"`
StateCreatedAt string `json:"state_created_at,omitempty" format:"date-time" example:"2024-01-15T10:30:00Z" doc:"Timestamp when the election first entered this state. Only Set if an election is running."`
}

type TransitionElectionStateInput struct {
Expand Down
13 changes: 12 additions & 1 deletion backend/internal/api/v1/models/otp.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package models

import "net/http"
import (
"net/http"
"time"
)

type GenerateOTPInput struct {
Body struct {
Expand All @@ -20,4 +23,12 @@ type SubmitOTPInput struct {

type SubmitOTPResponse struct {
SetCookie http.Cookie `header:"Set-Cookie"`

Body SubmitOTPResponseBody
}

type SubmitOTPResponseBody struct {
Zid string `json:"zid" doc:"User zID" pattern:"^z[0-9]{7}$" example:"z0000000"`
Expiry time.Time `json:"expiry" format:"date-time" example:"2024-01-15T10:30:00Z" doc:"Timestamp when your session expires."`
IsAdmin bool `json:"is_admin" doc:"Admin status"`
}
4 changes: 2 additions & 2 deletions backend/internal/api/v1/routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -122,15 +122,15 @@ func Register(api huma.API, deps HandlerDependencies) {
Method: http.MethodPost,
Path: "/elections",
Summary: "Create an election",
Tags: []string{"Elections"},
Tags: []string{"Admin"},
}, handlers.CreateElection(deps.Logger, deps.ElectionStore))

huma.Register(adminRoutes, huma.Operation{
OperationID: "set-election-members",
Method: http.MethodPut,
Path: "/elections/{election_id}/members",
Summary: "Set the member list for an election",
Tags: []string{"Elections"},
Tags: []string{"Admin"},
}, handlers.ElectionMemberListSet(deps.Logger, deps.ElectionStore))

huma.Register(adminRoutes, huma.Operation{
Expand Down
2 changes: 1 addition & 1 deletion backend/internal/store/nomination_store.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ type SubmitNomination struct {
DiscordUsername string `db:"discord_username" json:"discord_username" maxLength:"32" example:"johndoe"`
ExecutiveRoles []string `db:"executive_roles" json:"executive_roles" minItems:"1" maxItems:"6" uniqueItems:"true" enum:"president,secretary,treasurer,arc_delegate,edi_officer,grievance_officer" example:"[\"president\", \"secretary\"]"`
CandidateStatement string `db:"candidate_statement" json:"candidate_statement" required:"true" minLength:"50" maxLength:"2000" example:"I am running for president because..."`
URL *string `db:"url" json:"url" format:"uri" required:"false" example:"https://johndoe.com"`
URL *string `db:"url" json:"url,omitempty" format:"uri" required:"false" example:"https://johndoe.com"`
}

type NominationStore interface {
Expand Down