Skip to content

Commit 759b771

Browse files
committed
fix: evoting groups
1 parent 570904e commit 759b771

File tree

4 files changed

+144
-11
lines changed

4 files changed

+144
-11
lines changed

platforms/eVoting/src/app/(app)/page.tsx

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,11 @@ export default function Home() {
153153
>
154154
Visibility {getSortIcon("visibility")}
155155
</th>
156+
<th
157+
className="text-left py-3 px-4 font-medium text-gray-700 cursor-pointer hover:bg-gray-50"
158+
>
159+
Group
160+
</th>
156161
<th
157162
className="text-left py-3 px-4 font-medium text-gray-700 cursor-pointer hover:bg-gray-50"
158163
onClick={() => handleSort("status")}
@@ -196,6 +201,15 @@ export default function Home() {
196201
)}
197202
</Badge>
198203
</td>
204+
<td className="py-3 px-4">
205+
{poll.group ? (
206+
<Badge variant="outline" className="text-xs">
207+
{poll.group.name}
208+
</Badge>
209+
) : (
210+
<span className="text-xs text-gray-400">No group</span>
211+
)}
212+
</td>
199213
<td className="py-3 px-4">
200214
<Badge variant={isActive ? "success" : "warning"}>
201215
{isActive ? "Active" : "Ended"}

platforms/evoting-api/src/controllers/PollController.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,12 @@ export class PollController {
3535
getPollById = async (req: Request, res: Response) => {
3636
try {
3737
const { id } = req.params;
38+
const userId = (req as any).user?.id; // Get user ID from auth middleware
3839

39-
const poll = await this.pollService.getPollById(id);
40+
const poll = await this.pollService.getPollByIdWithAccessCheck(id, userId);
4041

4142
if (!poll) {
42-
return res.status(404).json({ error: "Poll not found" });
43+
return res.status(404).json({ error: "Poll not found or access denied" });
4344
}
4445

4546
res.json(poll);

platforms/evoting-api/src/services/PollService.ts

Lines changed: 58 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,10 +52,24 @@ export class PollService {
5252
}
5353
});
5454

55+
// Get group information for polls that have groupId
56+
const pollsWithGroups = await Promise.all(
57+
allPolls.map(async (poll) => {
58+
if (poll.groupId) {
59+
const group = await this.groupRepository.findOne({
60+
where: { id: poll.groupId },
61+
select: ['id', 'name', 'description']
62+
});
63+
return { ...poll, group };
64+
}
65+
return poll;
66+
})
67+
);
68+
5569
// Filter polls based on user's group memberships
56-
let filteredPolls = allPolls;
70+
let filteredPolls = pollsWithGroups;
5771
if (userId && userGroupIds.length > 0) {
58-
filteredPolls = allPolls.filter(poll => {
72+
filteredPolls = filteredPolls.filter(poll => {
5973
// Show polls that:
6074
// 1. Have no groupId (public polls)
6175
// 2. Belong to groups where user is a member/admin/participant
@@ -66,7 +80,7 @@ export class PollService {
6680
});
6781
} else if (userId) {
6882
// If user has no group memberships, only show their own polls and public polls
69-
filteredPolls = allPolls.filter(poll => !poll.groupId || poll.creatorId === userId);
83+
filteredPolls = filteredPolls.filter(poll => !poll.groupId || poll.creatorId === userId);
7084
}
7185

7286
// Custom sorting based on sortField and sortDirection
@@ -140,6 +154,47 @@ export class PollService {
140154
});
141155
}
142156

157+
/**
158+
* Get poll by ID with group information and check if user can access it
159+
*/
160+
async getPollByIdWithAccessCheck(id: string, userId?: string): Promise<Poll | null> {
161+
const poll = await this.pollRepository.findOne({
162+
where: { id },
163+
relations: ["creator"]
164+
});
165+
166+
if (!poll) {
167+
return null;
168+
}
169+
170+
// If poll has no group, it's public - anyone can access
171+
if (!poll.groupId) {
172+
return poll;
173+
}
174+
175+
// If no userId provided, don't show group polls
176+
if (!userId) {
177+
return null;
178+
}
179+
180+
// Check if user is a member, admin, or participant of the group
181+
const group = await this.groupRepository
182+
.createQueryBuilder('group')
183+
.leftJoin('group.members', 'member')
184+
.leftJoin('group.admins', 'admin')
185+
.leftJoin('group.participants', 'participant')
186+
.where('group.id = :groupId', { groupId: poll.groupId })
187+
.andWhere('(member.id = :userId OR admin.id = :userId OR participant.id = :userId)', { userId })
188+
.getOne();
189+
190+
// If user is not in the group, don't show the poll
191+
if (!group) {
192+
return null;
193+
}
194+
195+
return poll;
196+
}
197+
143198
async createPoll(pollData: {
144199
title: string;
145200
mode: string;

platforms/evoting-api/src/services/VoteService.ts

Lines changed: 69 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,14 @@ import { AppDataSource } from "../database/data-source";
33
import { Vote, VoteDataByMode, NormalVoteData, PointVoteData, RankVoteData } from "../database/entities/Vote";
44
import { Poll } from "../database/entities/Poll";
55
import { User } from "../database/entities/User";
6+
import { Group } from "../database/entities/Group";
67
import { VotingSystem, VoteData } from 'blindvote';
78

89
export class VoteService {
910
private voteRepository: Repository<Vote>;
1011
private pollRepository: Repository<Poll>;
1112
private userRepository: Repository<User>;
13+
private groupRepository: Repository<Group>;
1214

1315
// Store VotingSystem instances per poll
1416
private votingSystems = new Map<string, VotingSystem>();
@@ -17,11 +19,50 @@ export class VoteService {
1719
this.voteRepository = AppDataSource.getRepository(Vote);
1820
this.pollRepository = AppDataSource.getRepository(Poll);
1921
this.userRepository = AppDataSource.getRepository(User);
22+
this.groupRepository = AppDataSource.getRepository(Group);
23+
}
24+
25+
/**
26+
* Check if a user can vote on a poll based on group membership
27+
*/
28+
private async canUserVote(pollId: string, userId: string): Promise<boolean> {
29+
const poll = await this.pollRepository.findOne({
30+
where: { id: pollId },
31+
relations: ['creator']
32+
});
33+
34+
if (!poll) {
35+
throw new Error('Poll not found');
36+
}
37+
38+
// If poll has no group, it's a public poll - anyone can vote
39+
if (!poll.groupId) {
40+
return true;
41+
}
42+
43+
// If poll has a group, check if user is a member, admin, or participant
44+
const group = await this.groupRepository
45+
.createQueryBuilder('group')
46+
.leftJoin('group.members', 'member')
47+
.leftJoin('group.admins', 'admin')
48+
.leftJoin('group.participants', 'participant')
49+
.where('group.id = :groupId', { groupId: poll.groupId })
50+
.andWhere('(member.id = :userId OR admin.id = :userId OR participant.id = :userId)', { userId })
51+
.getOne();
52+
53+
if (!group) {
54+
throw new Error('User is not a member, admin, or participant of the group associated with this poll');
55+
}
56+
57+
return true;
2058
}
2159

2260
// ===== NON-BLIND VOTING METHODS (for normal/point/rank modes) =====
2361

2462
async createVote(pollId: string, userId: string, voteData: VoteData, mode: "normal" | "point" | "rank" = "normal"): Promise<Vote> {
63+
// First check if user can vote on this poll
64+
await this.canUserVote(pollId, userId);
65+
2566
const poll = await this.pollRepository.findOne({
2667
where: { id: pollId }
2768
});
@@ -313,6 +354,15 @@ export class VoteService {
313354

314355
async submitBlindVote(pollId: string, voterId: string, voteData: any) {
315356
try {
357+
// First check if user can vote on this poll (group membership check)
358+
const user = await this.userRepository.findOne({ where: { ename: voterId } });
359+
if (!user) {
360+
throw new Error(`User with ename ${voterId} not found. User must exist before submitting blind vote.`);
361+
}
362+
363+
// Check group membership using the existing method
364+
await this.canUserVote(pollId, user.id);
365+
316366
const votingSystem = this.getVotingSystemForPoll(pollId);
317367

318368
// Get poll to find options
@@ -374,12 +424,7 @@ export class VoteService {
374424
revealed: false
375425
};
376426

377-
// For blind voting, look up the user by their ename (W3ID)
378-
const user = await this.userRepository.findOne({ where: { ename: voterId } });
379-
if (!user) {
380-
throw new Error(`User with ename ${voterId} not found. User must exist before submitting blind vote.`);
381-
}
382-
427+
// User is already fetched from the group membership check above
383428
const vote = this.voteRepository.create({
384429
poll: { id: pollId },
385430
user: { id: user.id },
@@ -515,6 +560,15 @@ export class VoteService {
515560

516561
async registerBlindVoteVoter(pollId: string, voterId: string) {
517562
try {
563+
// First check if user can vote on this poll (group membership check)
564+
const user = await this.userRepository.findOne({ where: { ename: voterId } });
565+
if (!user) {
566+
throw new Error(`User with ename ${voterId} not found. User must exist before registering for blind voting.`);
567+
}
568+
569+
// Check group membership using the existing method
570+
await this.canUserVote(pollId, user.id);
571+
518572
const votingSystem = this.getVotingSystemForPoll(pollId);
519573

520574
// Get poll details
@@ -561,6 +615,15 @@ export class VoteService {
561615
// Generate vote data for a voter (used by eID wallet)
562616
async generateVoteData(pollId: string, voterId: string, chosenOptionId: string) {
563617
try {
618+
// First check if user can vote on this poll (group membership check)
619+
const user = await this.userRepository.findOne({ where: { ename: voterId } });
620+
if (!user) {
621+
throw new Error(`User with ename ${voterId} not found. User must exist before generating vote data.`);
622+
}
623+
624+
// Check group membership using the existing method
625+
await this.canUserVote(pollId, user.id);
626+
564627
const votingSystem = this.getVotingSystemForPoll(pollId);
565628

566629
// Get poll details

0 commit comments

Comments
 (0)