Skip to content

Commit c97717b

Browse files
committed
chore: restrict eVoting to chartered groups
1 parent 54482b8 commit c97717b

File tree

4 files changed

+143
-12
lines changed

4 files changed

+143
-12
lines changed

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

Lines changed: 116 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,10 @@ const createPollSchema = z.object({
2929
title: z.string().min(1, "Poll title is required"),
3030
mode: z.enum(["normal", "point", "rank"]),
3131
visibility: z.enum(["public", "private"]),
32-
groupId: z.string().min(1, "Please select a group"),
32+
groupId: z.string().min(1, "Please select a group").refine((val) => {
33+
// This will be validated in the onSubmit function with the actual groups data
34+
return true;
35+
}, "Please select a valid group"),
3336
options: z
3437
.array(z.string().min(1, "Option cannot be empty"))
3538
.min(2, "At least 2 options required"),
@@ -70,13 +73,28 @@ export default function CreatePoll() {
7073
},
7174
});
7275

76+
// Helper function to sort groups: chartered first, then by name
77+
const sortGroupsByCharterStatus = (groups: Group[]) => {
78+
return [...groups].sort((a, b) => {
79+
const aChartered = a.charter && a.charter.trim() !== "";
80+
const bChartered = b.charter && b.charter.trim() !== "";
81+
82+
if (aChartered && !bChartered) return -1;
83+
if (!aChartered && bChartered) return 1;
84+
85+
// If both have same charter status, sort by name
86+
return a.name.localeCompare(b.name);
87+
});
88+
};
89+
7390
// Fetch user's groups on component mount
7491
useEffect(() => {
7592
const fetchGroups = async () => {
7693
try {
7794
const userGroups = await pollApi.getUserGroups();
78-
// Ensure groups is always an array
79-
setGroups(Array.isArray(userGroups) ? userGroups : []);
95+
// Ensure groups is always an array and sort by charter status
96+
const sortedGroups = Array.isArray(userGroups) ? sortGroupsByCharterStatus(userGroups) : [];
97+
setGroups(sortedGroups);
8098
} catch (error) {
8199
console.error("Failed to fetch groups:", error);
82100
setGroups([]); // Set empty array on error
@@ -128,6 +146,26 @@ export default function CreatePoll() {
128146
const onSubmit = async (data: CreatePollForm) => {
129147
setIsSubmitting(true);
130148
try {
149+
// Validate that the selected group is chartered
150+
const selectedGroup = groups.find(group => group.id === data.groupId);
151+
if (!selectedGroup) {
152+
toast({
153+
title: "Error",
154+
description: "Please select a valid group",
155+
variant: "destructive",
156+
});
157+
return;
158+
}
159+
160+
if (!selectedGroup.charter || selectedGroup.charter.trim() === "") {
161+
toast({
162+
title: "Error",
163+
description: "Only chartered groups can create polls. Please select a group with a charter.",
164+
variant: "destructive",
165+
});
166+
return;
167+
}
168+
131169
// Convert local deadline to UTC before sending to backend
132170
let utcDeadline: string | undefined;
133171
if (data.deadline) {
@@ -152,11 +190,18 @@ export default function CreatePoll() {
152190
});
153191

154192
router.push("/");
155-
} catch (error) {
193+
} catch (error: any) {
156194
console.error("Failed to create poll:", error);
195+
196+
// Show specific error message from backend if available
197+
let errorMessage = "Failed to create poll. Please try again.";
198+
if (error?.response?.data?.error) {
199+
errorMessage = error.response.data.error;
200+
}
201+
157202
toast({
158203
title: "Error",
159-
description: "Failed to create poll. Please try again.",
204+
description: errorMessage,
160205
variant: "destructive",
161206
});
162207
} finally {
@@ -197,6 +242,15 @@ export default function CreatePoll() {
197242
<Label className="text-sm font-semibold text-gray-700">
198243
Group
199244
</Label>
245+
{!isLoadingGroups && groups.length > 0 && (
246+
<div className="mt-1 mb-2 text-xs text-gray-600">
247+
{(() => {
248+
const charteredCount = groups.filter(group => group.charter && group.charter.trim() !== "").length;
249+
const totalCount = groups.length;
250+
return `${charteredCount} of ${totalCount} groups are chartered`;
251+
})()}
252+
</div>
253+
)}
200254
<Select onValueChange={(value) => setValue("groupId", value)}>
201255
<SelectTrigger className="w-full mt-2">
202256
<SelectValue placeholder="Select a group" />
@@ -207,14 +261,66 @@ export default function CreatePoll() {
207261
) : !Array.isArray(groups) || groups.length === 0 ? (
208262
<SelectItem value="no-groups" disabled>No groups found. Create one!</SelectItem>
209263
) : (
210-
groups.map((group) => (
211-
<SelectItem key={group.id} value={group.id}>
212-
{group.name}
213-
</SelectItem>
214-
))
264+
(() => {
265+
const charteredGroups = groups.filter(group => group.charter && group.charter.trim() !== "");
266+
const nonCharteredGroups = groups.filter(group => !group.charter || group.charter.trim() === "");
267+
268+
return (
269+
<>
270+
{/* Chartered Groups */}
271+
{charteredGroups.length > 0 && (
272+
<>
273+
<div className="px-2 py-1.5 text-xs font-semibold text-gray-500 bg-gray-50">
274+
Chartered Groups
275+
</div>
276+
{charteredGroups.map((group) => (
277+
<SelectItem
278+
key={group.id}
279+
value={group.id}
280+
>
281+
<div className="flex items-center justify-between w-full">
282+
<span>{group.name}</span>
283+
<span className="text-xs text-green-600 ml-2">
284+
✓ Chartered
285+
</span>
286+
</div>
287+
</SelectItem>
288+
))}
289+
</>
290+
)}
291+
292+
{/* Non-Chartered Groups */}
293+
{nonCharteredGroups.length > 0 && (
294+
<>
295+
<div className="px-2 py-1.5 text-xs font-semibold text-gray-500 bg-gray-50">
296+
Non-Chartered Groups
297+
</div>
298+
{nonCharteredGroups.map((group) => (
299+
<SelectItem
300+
key={group.id}
301+
value={group.id}
302+
disabled
303+
className="opacity-60 cursor-not-allowed"
304+
>
305+
<div className="flex items-center justify-between w-full">
306+
<span>{group.name}</span>
307+
<span className="text-xs text-gray-500 ml-2">
308+
(Not chartered)
309+
</span>
310+
</div>
311+
</SelectItem>
312+
))}
313+
</>
314+
)}
315+
</>
316+
);
317+
})()
215318
)}
216319
</SelectContent>
217320
</Select>
321+
<p className="mt-1 text-sm text-gray-500">
322+
Only chartered groups can create polls. Groups without a charter will be disabled.
323+
</p>
218324
{errors.groupId && (
219325
<p className="mt-1 text-sm text-red-600">
220326
{errors.groupId.message}

platforms/eVoting/src/lib/pollApi.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ export interface Group {
5353
owner: string;
5454
isPrivate: boolean;
5555
visibility: "public" | "private" | "restricted";
56+
charter?: string; // Markdown content for the group charter
5657
createdAt: string;
5758
updatedAt: string;
5859
}

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,14 @@ export class PollController {
7474
res.status(201).json(poll);
7575
} catch (error) {
7676
console.error("Error creating poll:", error);
77+
78+
// Handle specific charter validation error
79+
if (error instanceof Error && error.message === "Only chartered groups can create polls") {
80+
return res.status(400).json({
81+
error: "Only chartered groups can create polls. Please select a group with a charter."
82+
});
83+
}
84+
7785
res.status(500).json({ error: "Failed to create poll" });
7886
}
7987
};

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

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,13 @@ import { MessageService } from "./MessageService";
88
export class PollService {
99
private pollRepository: Repository<Poll>;
1010
private userRepository: Repository<User>;
11+
private groupRepository: Repository<Group>;
1112
private messageService: MessageService;
1213

1314
constructor() {
1415
this.pollRepository = AppDataSource.getRepository(Poll);
1516
this.userRepository = AppDataSource.getRepository(User);
17+
this.groupRepository = AppDataSource.getRepository(Group);
1618
this.messageService = new MessageService();
1719
}
1820

@@ -30,8 +32,7 @@ export class PollService {
3032
let userGroupIds: string[] = [];
3133
if (userId) {
3234
// Get groups where user is a member, admin, or participant
33-
const groupRepository = AppDataSource.getRepository(Group);
34-
const userGroups = await groupRepository
35+
const userGroups = await this.groupRepository
3536
.createQueryBuilder('group')
3637
.leftJoin('group.members', 'member')
3738
.leftJoin('group.admins', 'admin')
@@ -158,6 +159,21 @@ export class PollService {
158159
throw new Error("Creator not found");
159160
}
160161

162+
// If a groupId is provided, validate that the group is chartered
163+
if (pollData.groupId) {
164+
const group = await this.groupRepository.findOne({
165+
where: { id: pollData.groupId }
166+
});
167+
168+
if (!group) {
169+
throw new Error("Group not found");
170+
}
171+
172+
if (!group.charter || group.charter.trim() === "") {
173+
throw new Error("Only chartered groups can create polls");
174+
}
175+
}
176+
161177
const pollDataForEntity = {
162178
title: pollData.title,
163179
mode: pollData.mode as "normal" | "point" | "rank",

0 commit comments

Comments
 (0)