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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ written while it was being built. See [RELEASING.md](RELEASING.md).

### Fixed

- Participants can see published results after voting has closed; the results
page used to fail with an error once an organizer turned voting off.

## [0.12.1](https://github.com/SwissDataScienceCenter/hackagon/compare/v0.12.0...v0.12.1) - 2026-10-27

## [0.12.0](https://github.com/SwissDataScienceCenter/hackagon/compare/v0.11.0...v0.12.0) - 2026-10-23
Expand Down
21 changes: 18 additions & 3 deletions components/backend/internal/service/vote_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,13 +48,28 @@ func (s *VoteService) ListVoteCategories(
return nil, status.Errorf(codes.InvalidArgument, "invalid hackathon_id: %v", err)
}

if err := s.enforcer.RequirePermission(
// Results are fetched per category, so anyone who may read results must
// also be able to list the categories, even once voting is off.
canReadCategories, err := s.enforcer.Enforce(
ctx,
hackathonID.String(),
m.VoteCategory,
m.Read,
); err != nil {
return nil, err
)
if err != nil {
slog.Error("enforce list vote categories", "err", err)

return nil, status.Error(codes.Internal, "authorization error")
}
if !canReadCategories {
if err := s.enforcer.RequirePermission(
ctx,
hackathonID.String(),
m.VoteResult,
m.Read,
); err != nil {
return nil, err
}
}

categories, err := s.dbClient.VoteCategory.Query().
Expand Down
49 changes: 49 additions & 0 deletions components/backend/internal/service/vote_service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,55 @@ var _ = Describe("VoteService", func() {
Expect(err).NotTo(HaveOccurred())
Expect(resp.GetVoteCategories()).To(BeEmpty())
})

Context("as a member", func() {
var memberCtx context.Context
BeforeEach(func() {
createVoteCategory(
hackathonID,
"Best",
voteEntities.VotingMethod_VOTING_METHOD_SINGLE_CHOICE,
voteEntities.VoterType_VOTER_TYPE_ALL_PARTICIPANTS,
nil,
)
_, kc := createAndJoin(hackathonID, "member")
memberCtx = metadata.NewOutgoingContext(
context.Background(),
metadata.Pairs("authorization", "Bearer "+testutils.CreateTestJWTToken(kc)),
)
})

It("lists categories while voting is enabled", func() {
setCapabilities(hackathonID, hackathonEntities.Capability_CAPABILITY_VOTE)
resp, err := voteClient.ListVoteCategories(
memberCtx,
&voteMsgs.ListVoteCategoriesRequest{HackathonId: hackathonID},
)
Expect(err).NotTo(HaveOccurred())
Expect(resp.GetVoteCategories()).To(HaveLen(1))
})

It("lists categories when only results are visible", func() {
setCapabilities(
hackathonID,
hackathonEntities.Capability_CAPABILITY_VIEW_RESULTS,
)
resp, err := voteClient.ListVoteCategories(
memberCtx,
&voteMsgs.ListVoteCategoriesRequest{HackathonId: hackathonID},
)
Expect(err).NotTo(HaveOccurred())
Expect(resp.GetVoteCategories()).To(HaveLen(1))
})

It("is denied when neither voting nor results are enabled", func() {
_, err := voteClient.ListVoteCategories(
memberCtx,
&voteMsgs.ListVoteCategoriesRequest{HackathonId: hackathonID},
)
Expect(status.Code(err)).To(Equal(codes.PermissionDenied))
})
})
})

Describe("EditVoteCategory", func() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,25 @@ export const load: PageServerLoad = async (event) => {
// Every category, including ranked and jury ones the booth cannot cast a
// vote for — results are results however they were produced, and a category
// decided elsewhere (a jury meeting off the platform, say) still deserves to
// show its podium. Reading a result needs only `vote_result:read`, which the
// capability grants to every member.
const { voteCategories } = await vote.listVoteCategories({
hackathonId: event.params.id,
})
// show its podium. Listing categories takes `vote_category:read` *or*
// `vote_result:read`, and the capability grants the latter to every member,
// so this works after voting has closed.
//
// The catch is a guard, same as the per-category one below: a refusal here
// once took the whole page down with a 500. Losing the podiums still leaves
// the entries directory.
let voteCategories: Awaited<
ReturnType<typeof vote.listVoteCategories>
>["voteCategories"] = []
try {
;({ voteCategories } = await vote.listVoteCategories({
hackathonId: event.params.id,
}))
} catch (e) {
if (!(e instanceof ClientError && e.code === Status.PERMISSION_DENIED)) {
throw e
}
}

const categories = await Promise.all(
voteCategories.map(async (c) => {
Expand Down
Loading