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
31 changes: 29 additions & 2 deletions platforms/evoting/api/scripts/seed.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -486,7 +486,33 @@ async function seed() {

console.log(`\n📊 Polls created: ${pollsCreated}, Votes created: ${votesCreated}\n`);

// Optionally add a custom user to all groups
// Auto-add all non-seed users (created via dev-sandbox auth/webhooks) to all groups
const seedEnames = new Set(seedUsers.map(u => u.ename));
const allUsers = await userRepo.find();
const nonSeedUsers = allUsers.filter(u => !seedEnames.has(u.ename));

if (nonSeedUsers.length > 0) {
console.log(`👤 Adding ${nonSeedUsers.length} existing user(s) to all groups...`);

const groupsToUpdate = await groupRepo.find({
relations: ["members", "participants"],
});

for (const user of nonSeedUsers) {
for (const group of groupsToUpdate) {
const isAlreadyMember = group.members.some((m) => m.id === user.id);

if (!isAlreadyMember) {
group.members.push(user);
group.participants.push(user);
await groupRepo.save(group);
console.log(` ✅ Added "${user.name || user.ename}" to "${group.name}"`);
}
}
}
}

// Also support explicit ename argument
const customEname = process.argv[2];
if (customEname) {
console.log(`\n👤 Adding custom user to all groups...`);
Expand DownExpand Up@@ -532,7 +558,8 @@ async function seed() {

console.log("\n🎉 Seed completed successfully!\n");
console.log("Summary:");
console.log(` - ${seedUsers.length} users`);
console.log(` - ${seedUsers.length} seed users`);
console.log(` - ${nonSeedUsers.length} existing user(s) added to all groups`);
console.log(` - ${seedGroups.length} groups (with charters)`);
console.log(` - ${seedPolls.length} polls (various modes: normal, point, rank)`);
console.log(` - Includes public and private polls with sample votes`);
Expand Down
46 changes: 46 additions & 0 deletions platforms/evoting/api/src/controllers/PollController.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -156,4 +156,50 @@ export class PollController {
res.status(500).json({ error: "Failed to fetch user polls" });
}
};

startPoll = async (req: Request, res: Response) => {
try {
const { id } = req.params;
const userId = (req as any).user.id;
const poll = await this.pollService.startPoll(id, userId);
res.json(poll);
} catch (error) {
console.error("Error starting poll:", error);
if (error instanceof Error) {
if (error.message === "Poll not found") {
return res.status(404).json({ error: error.message });
}
if (error.message === "Not authorized to start this poll") {
return res.status(403).json({ error: error.message });
}
if (error.message === "Only draft polls can be started") {
return res.status(400).json({ error: error.message });
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
res.status(500).json({ error: "Failed to start poll" });
}
};

endPoll = async (req: Request, res: Response) => {
try {
const { id } = req.params;
const userId = (req as any).user.id;
const poll = await this.pollService.endPoll(id, userId);
res.json(poll);
} catch (error) {
console.error("Error ending poll:", error);
if (error instanceof Error) {
if (error.message === "Poll not found") {
return res.status(404).json({ error: error.message });
}
if (error.message === "Not authorized to end this poll") {
return res.status(403).json({ error: error.message });
}
if (error.message === "Only active polls can be ended") {
return res.status(400).json({ error: error.message });
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
res.status(500).json({ error: "Failed to end poll" });
}
};
}
6 changes: 6 additions & 0 deletions platforms/evoting/api/src/database/entities/Poll.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -40,6 +40,12 @@ export class Poll {
@Column("simple-array")
options!: string[]; // stored as comma-separated values

@Column("enum", {
enum: ["draft", "active", "ended"],
default: "active",
})
status!: "draft" | "active" | "ended";

@Column({ type: "timestamp", nullable: true })
deadline!: Date | null;

Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
import { MigrationInterface, QueryRunner } from "typeorm";

export class Migration1772851200000 implements MigrationInterface {
name = 'Migration1772851200000'

public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`CREATE TYPE "public"."polls_status_enum" AS ENUM('draft', 'active', 'ended')`);
await queryRunner.query(`ALTER TABLE "polls" ADD "status" "public"."polls_status_enum" NOT NULL DEFAULT 'active'`);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "polls" DROP COLUMN "status"`);
await queryRunner.query(`DROP TYPE "public"."polls_status_enum"`);
}
}
2 changes: 2 additions & 0 deletions platforms/evoting/api/src/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -209,6 +209,8 @@ app.get("/api/polls/my", authGuard, pollController.getPollsByCreator);
app.post("/api/polls", authGuard, pollController.createPoll);
app.put("/api/polls/:id", authGuard, pollController.updatePoll);
app.delete("/api/polls/:id", authGuard, pollController.deletePoll);
app.post("/api/polls/:id/start", authGuard, pollController.startPoll);
app.post("/api/polls/:id/end", authGuard, pollController.endPoll);

// Vote routes
app.post("/api/votes", voteController.createVote.bind(voteController)); // Create normal/point/rank vote (old format)
Expand Down
4 changes: 2 additions & 2 deletions platforms/evoting/api/src/services/DeadlineCheckService.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -80,8 +80,8 @@ export class DeadlineCheckService {
voteId: poll.id
});

// Mark that we've sent the deadline message
await this.pollRepository.update(poll.id, { deadlineMessageSent: true });
// Mark that we've sent the deadline message and set status to ended
await this.pollRepository.update(poll.id, { deadlineMessageSent: true, status: "ended" });

console.log(`Successfully sent deadline message for poll: ${poll.title} (${poll.id})`);

Expand Down
73 changes: 68 additions & 5 deletions platforms/evoting/api/src/services/PollService.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -86,8 +86,8 @@ export class PollService {
// Custom sorting based on sortField and sortDirection
const sortedPolls = filteredPolls.sort((a, b) => {
const now = new Date();
const aIsActive = !a.deadline || new Date(a.deadline) > now;
const bIsActive = !b.deadline || new Date(b.deadline) > now;
const aIsActive = a.status === "active" || (a.status !== "ended" && a.status !== "draft" && (!a.deadline || new Date(a.deadline) > now));
const bIsActive = b.status === "active" || (b.status !== "ended" && b.status !== "draft" && (!b.deadline || new Date(b.deadline) > now));

// Apply the user's chosen sorting
let comparison = 0;
Expand DownExpand Up@@ -218,13 +218,15 @@ export class PollService {
throw new Error("Blind voting (private visibility) cannot be combined with eReputation weighted voting.");
}

const hasDeadline = !!pollData.deadline;
const pollDataForEntity = {
title: pollData.title,
mode: pollData.mode as "normal" | "point" | "rank",
visibility: pollData.visibility as "public" | "private",
votingWeight: votingWeight,
options: pollData.options,
deadline: pollData.deadline ? new Date(pollData.deadline) : null,
status: hasDeadline ? "active" as const : "draft" as const,
deadline: hasDeadline ? new Date(pollData.deadline!) : null,
creator,
creatorId: pollData.creatorId,
groupId: pollData.groupId || null
Expand All@@ -237,8 +239,8 @@ export class PollService {
const savedPoll = await this.pollRepository.save(poll);
console.log('🔍 Poll saved to database:', savedPoll);

// Create a system message about the new vote
if (pollData.groupId) {
// Create a system message about the new vote (only for active polls, not drafts)
if (pollData.groupId && hasDeadline) {
await this.messageService.createVoteCreatedMessage(pollData.groupId, pollData.title, savedPoll.id, creator.name, savedPoll.deadline);
}

Expand DownExpand Up@@ -301,6 +303,67 @@ export class PollService {



/**
* Manually start a poll (draft → active). Only creator can do this.
*/
async startPoll(id: string, userId: string): Promise<Poll> {
const poll = await this.getPollById(id);
if (!poll) {
throw new Error("Poll not found");
}
if (poll.creatorId !== userId) {
throw new Error("Not authorized to start this poll");
}
if (poll.status !== "draft") {
throw new Error("Only draft polls can be started");
}

await this.pollRepository.update(id, { status: "active" });

// Send system message that voting is now open
if (poll.groupId) {
await this.messageService.createVoteCreatedMessage(
poll.groupId,
poll.title,
poll.id,
poll.creator.name,
poll.deadline
);
}

return (await this.getPollById(id))!;
}

/**
* Manually end a poll (active → ended). Only creator can do this.
*/
async endPoll(id: string, userId: string): Promise<Poll> {
const poll = await this.getPollById(id);
if (!poll) {
throw new Error("Poll not found");
}
if (poll.creatorId !== userId) {
throw new Error("Not authorized to end this poll");
}
if (poll.status !== "active") {
throw new Error("Only active polls can be ended");
}

await this.pollRepository.update(id, { status: "ended", deadlineMessageSent: true });

// Send system message that voting has ended
if (poll.groupId) {
const voteUrl = `${process.env.PUBLIC_EVOTING_URL || 'http://localhost:3000'}/${poll.id}`;
await this.messageService.createSystemMessage({
text: `eVoting Platform: Vote has been manually closed!\n\n"${poll.title}"\n\nVote ID: ${poll.id}\n\nClosed by: ${poll.creator.name}\n\n<a href="${voteUrl}" target="_blank">View results here</a>`,
groupId: poll.groupId,
voteId: poll.id
});
}

return (await this.getPollById(id))!;
}

/**
* Get polls by group ID
*/
Expand Down
24 changes: 24 additions & 0 deletions platforms/evoting/api/src/services/VoteService.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -115,6 +115,14 @@ export class VoteService {
throw new Error('Poll not found');
}

// Prevent voting on polls that are not active
if (poll.status === "draft") {
throw new Error('Voting has not started yet');
}
if (poll.status === "ended") {
throw new Error('Voting has ended');
}

// Check if user has already voted
const existingVote = await this.voteRepository.findOne({
where: { pollId, userId }
Expand DownExpand Up@@ -178,6 +186,14 @@ export class VoteService {
throw new Error('Poll not found');
}

// Prevent voting on polls that are not active
if (poll.status === "draft") {
throw new Error('Voting has not started yet');
}
if (poll.status === "ended") {
throw new Error('Voting has ended');
}

const delegation = await this.delegationRepository.findOne({
where: {
pollId,
Expand DownExpand Up@@ -660,6 +676,14 @@ export class VoteService {
throw new Error('Poll not found');
}

// Prevent voting on polls that are not active
if (poll.status === "draft") {
throw new Error('Voting has not started yet');
}
if (poll.status === "ended") {
throw new Error('Voting has ended');
}

// Create election if it doesn't exist
try {
const options = poll.options.map((opt: string, index: number) => `option_${index}`);
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
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
31 changes: 29 additions & 2 deletions platforms/evoting/api/scripts/seed.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -486,7 +486,33 @@ async function seed() {

console.log(`\n📊 Polls created: ${pollsCreated}, Votes created: ${votesCreated}\n`);

// Optionally add a custom user to all groups
// Auto-add all non-seed users (created via dev-sandbox auth/webhooks) to all groups
const seedEnames = new Set(seedUsers.map(u => u.ename));
const allUsers = await userRepo.find();
const nonSeedUsers = allUsers.filter(u => !seedEnames.has(u.ename));

if (nonSeedUsers.length > 0) {
console.log(`👤 Adding ${nonSeedUsers.length} existing user(s) to all groups...`);

const groupsToUpdate = await groupRepo.find({
relations: ["members", "participants"],
});

for (const user of nonSeedUsers) {
for (const group of groupsToUpdate) {
const isAlreadyMember = group.members.some((m) => m.id === user.id);

if (!isAlreadyMember) {
group.members.push(user);
group.participants.push(user);
await groupRepo.save(group);
console.log(` ✅ Added "${user.name || user.ename}" to "${group.name}"`);
}
}
}
}

// Also support explicit ename argument
const customEname = process.argv[2];
if (customEname) {
console.log(`\n👤 Adding custom user to all groups...`);
Expand DownExpand Up@@ -532,7 +558,8 @@ async function seed() {

console.log("\n🎉 Seed completed successfully!\n");
console.log("Summary:");
console.log(` - ${seedUsers.length} users`);
console.log(` - ${seedUsers.length} seed users`);
console.log(` - ${nonSeedUsers.length} existing user(s) added to all groups`);
console.log(` - ${seedGroups.length} groups (with charters)`);
console.log(` - ${seedPolls.length} polls (various modes: normal, point, rank)`);
console.log(` - Includes public and private polls with sample votes`);
Expand Down
46 changes: 46 additions & 0 deletions platforms/evoting/api/src/controllers/PollController.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -156,4 +156,50 @@ export class PollController {
res.status(500).json({ error: "Failed to fetch user polls" });
}
};

startPoll = async (req: Request, res: Response) => {
try {
const { id } = req.params;
const userId = (req as any).user.id;
const poll = await this.pollService.startPoll(id, userId);
res.json(poll);
} catch (error) {
console.error("Error starting poll:", error);
if (error instanceof Error) {
if (error.message === "Poll not found") {
return res.status(404).json({ error: error.message });
}
if (error.message === "Not authorized to start this poll") {
return res.status(403).json({ error: error.message });
}
if (error.message === "Only draft polls can be started") {
return res.status(400).json({ error: error.message });
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
res.status(500).json({ error: "Failed to start poll" });
}
};

endPoll = async (req: Request, res: Response) => {
try {
const { id } = req.params;
const userId = (req as any).user.id;
const poll = await this.pollService.endPoll(id, userId);
res.json(poll);
} catch (error) {
console.error("Error ending poll:", error);
if (error instanceof Error) {
if (error.message === "Poll not found") {
return res.status(404).json({ error: error.message });
}
if (error.message === "Not authorized to end this poll") {
return res.status(403).json({ error: error.message });
}
if (error.message === "Only active polls can be ended") {
return res.status(400).json({ error: error.message });
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
res.status(500).json({ error: "Failed to end poll" });
}
};
}
6 changes: 6 additions & 0 deletions platforms/evoting/api/src/database/entities/Poll.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -40,6 +40,12 @@ export class Poll {
@Column("simple-array")
options!: string[]; // stored as comma-separated values

@Column("enum", {
enum: ["draft", "active", "ended"],
default: "active",
})
status!: "draft" | "active" | "ended";

@Column({ type: "timestamp", nullable: true })
deadline!: Date | null;

Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
import { MigrationInterface, QueryRunner } from "typeorm";

export class Migration1772851200000 implements MigrationInterface {
name = 'Migration1772851200000'

public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`CREATE TYPE "public"."polls_status_enum" AS ENUM('draft', 'active', 'ended')`);
await queryRunner.query(`ALTER TABLE "polls" ADD "status" "public"."polls_status_enum" NOT NULL DEFAULT 'active'`);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "polls" DROP COLUMN "status"`);
await queryRunner.query(`DROP TYPE "public"."polls_status_enum"`);
}
}
2 changes: 2 additions & 0 deletions platforms/evoting/api/src/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -209,6 +209,8 @@ app.get("/api/polls/my", authGuard, pollController.getPollsByCreator);
app.post("/api/polls", authGuard, pollController.createPoll);
app.put("/api/polls/:id", authGuard, pollController.updatePoll);
app.delete("/api/polls/:id", authGuard, pollController.deletePoll);
app.post("/api/polls/:id/start", authGuard, pollController.startPoll);
app.post("/api/polls/:id/end", authGuard, pollController.endPoll);

// Vote routes
app.post("/api/votes", voteController.createVote.bind(voteController)); // Create normal/point/rank vote (old format)
Expand Down
4 changes: 2 additions & 2 deletions platforms/evoting/api/src/services/DeadlineCheckService.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -80,8 +80,8 @@ export class DeadlineCheckService {
voteId: poll.id
});

// Mark that we've sent the deadline message
await this.pollRepository.update(poll.id, { deadlineMessageSent: true });
// Mark that we've sent the deadline message and set status to ended
await this.pollRepository.update(poll.id, { deadlineMessageSent: true, status: "ended" });

console.log(`Successfully sent deadline message for poll: ${poll.title} (${poll.id})`);

Expand Down
73 changes: 68 additions & 5 deletions platforms/evoting/api/src/services/PollService.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -86,8 +86,8 @@ export class PollService {
// Custom sorting based on sortField and sortDirection
const sortedPolls = filteredPolls.sort((a, b) => {
const now = new Date();
const aIsActive = !a.deadline || new Date(a.deadline) > now;
const bIsActive = !b.deadline || new Date(b.deadline) > now;
const aIsActive = a.status === "active" || (a.status !== "ended" && a.status !== "draft" && (!a.deadline || new Date(a.deadline) > now));
const bIsActive = b.status === "active" || (b.status !== "ended" && b.status !== "draft" && (!b.deadline || new Date(b.deadline) > now));

// Apply the user's chosen sorting
let comparison = 0;
Expand DownExpand Up@@ -218,13 +218,15 @@ export class PollService {
throw new Error("Blind voting (private visibility) cannot be combined with eReputation weighted voting.");
}

const hasDeadline = !!pollData.deadline;
const pollDataForEntity = {
title: pollData.title,
mode: pollData.mode as "normal" | "point" | "rank",
visibility: pollData.visibility as "public" | "private",
votingWeight: votingWeight,
options: pollData.options,
deadline: pollData.deadline ? new Date(pollData.deadline) : null,
status: hasDeadline ? "active" as const : "draft" as const,
deadline: hasDeadline ? new Date(pollData.deadline!) : null,
creator,
creatorId: pollData.creatorId,
groupId: pollData.groupId || null
Expand All@@ -237,8 +239,8 @@ export class PollService {
const savedPoll = await this.pollRepository.save(poll);
console.log('🔍 Poll saved to database:', savedPoll);

// Create a system message about the new vote
if (pollData.groupId) {
// Create a system message about the new vote (only for active polls, not drafts)
if (pollData.groupId && hasDeadline) {
await this.messageService.createVoteCreatedMessage(pollData.groupId, pollData.title, savedPoll.id, creator.name, savedPoll.deadline);
}

Expand DownExpand Up@@ -301,6 +303,67 @@ export class PollService {



/**
* Manually start a poll (draft → active). Only creator can do this.
*/
async startPoll(id: string, userId: string): Promise<Poll> {
const poll = await this.getPollById(id);
if (!poll) {
throw new Error("Poll not found");
}
if (poll.creatorId !== userId) {
throw new Error("Not authorized to start this poll");
}
if (poll.status !== "draft") {
throw new Error("Only draft polls can be started");
}

await this.pollRepository.update(id, { status: "active" });

// Send system message that voting is now open
if (poll.groupId) {
await this.messageService.createVoteCreatedMessage(
poll.groupId,
poll.title,
poll.id,
poll.creator.name,
poll.deadline
);
}

return (await this.getPollById(id))!;
}

/**
* Manually end a poll (active → ended). Only creator can do this.
*/
async endPoll(id: string, userId: string): Promise<Poll> {
const poll = await this.getPollById(id);
if (!poll) {
throw new Error("Poll not found");
}
if (poll.creatorId !== userId) {
throw new Error("Not authorized to end this poll");
}
if (poll.status !== "active") {
throw new Error("Only active polls can be ended");
}

await this.pollRepository.update(id, { status: "ended", deadlineMessageSent: true });

// Send system message that voting has ended
if (poll.groupId) {
const voteUrl = `${process.env.PUBLIC_EVOTING_URL || 'http://localhost:3000'}/${poll.id}`;
await this.messageService.createSystemMessage({
text: `eVoting Platform: Vote has been manually closed!\n\n"${poll.title}"\n\nVote ID: ${poll.id}\n\nClosed by: ${poll.creator.name}\n\n<a href="${voteUrl}" target="_blank">View results here</a>`,
groupId: poll.groupId,
voteId: poll.id
});
}

return (await this.getPollById(id))!;
}

/**
* Get polls by group ID
*/
Expand Down
24 changes: 24 additions & 0 deletions platforms/evoting/api/src/services/VoteService.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -115,6 +115,14 @@ export class VoteService {
throw new Error('Poll not found');
}

// Prevent voting on polls that are not active
if (poll.status === "draft") {
throw new Error('Voting has not started yet');
}
if (poll.status === "ended") {
throw new Error('Voting has ended');
}

// Check if user has already voted
const existingVote = await this.voteRepository.findOne({
where: { pollId, userId }
Expand DownExpand Up@@ -178,6 +186,14 @@ export class VoteService {
throw new Error('Poll not found');
}

// Prevent voting on polls that are not active
if (poll.status === "draft") {
throw new Error('Voting has not started yet');
}
if (poll.status === "ended") {
throw new Error('Voting has ended');
}

const delegation = await this.delegationRepository.findOne({
where: {
pollId,
Expand DownExpand Up@@ -660,6 +676,14 @@ export class VoteService {
throw new Error('Poll not found');
}

// Prevent voting on polls that are not active
if (poll.status === "draft") {
throw new Error('Voting has not started yet');
}
if (poll.status === "ended") {
throw new Error('Voting has ended');
}

// Create election if it doesn't exist
try {
const options = poll.options.map((opt: string, index: number) => `option_${index}`);
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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
31 changes: 29 additions & 2 deletions platforms/evoting/api/scripts/seed.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -486,7 +486,33 @@ async function seed() {

console.log(`\n📊 Polls created: ${pollsCreated}, Votes created: ${votesCreated}\n`);

// Optionally add a custom user to all groups
// Auto-add all non-seed users (created via dev-sandbox auth/webhooks) to all groups
const seedEnames = new Set(seedUsers.map(u => u.ename));
const allUsers = await userRepo.find();
const nonSeedUsers = allUsers.filter(u => !seedEnames.has(u.ename));

if (nonSeedUsers.length > 0) {
console.log(`👤 Adding ${nonSeedUsers.length} existing user(s) to all groups...`);

const groupsToUpdate = await groupRepo.find({
relations: ["members", "participants"],
});

for (const user of nonSeedUsers) {
for (const group of groupsToUpdate) {
const isAlreadyMember = group.members.some((m) => m.id === user.id);

if (!isAlreadyMember) {
group.members.push(user);
group.participants.push(user);
await groupRepo.save(group);
console.log(` ✅ Added "${user.name || user.ename}" to "${group.name}"`);
}
}
}
}

// Also support explicit ename argument
const customEname = process.argv[2];
if (customEname) {
console.log(`\n👤 Adding custom user to all groups...`);
Expand DownExpand Up@@ -532,7 +558,8 @@ async function seed() {

console.log("\n🎉 Seed completed successfully!\n");
console.log("Summary:");
console.log(` - ${seedUsers.length} users`);
console.log(` - ${seedUsers.length} seed users`);
console.log(` - ${nonSeedUsers.length} existing user(s) added to all groups`);
console.log(` - ${seedGroups.length} groups (with charters)`);
console.log(` - ${seedPolls.length} polls (various modes: normal, point, rank)`);
console.log(` - Includes public and private polls with sample votes`);
Expand Down
46 changes: 46 additions & 0 deletions platforms/evoting/api/src/controllers/PollController.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -156,4 +156,50 @@ export class PollController {
res.status(500).json({ error: "Failed to fetch user polls" });
}
};

startPoll = async (req: Request, res: Response) => {
try {
const { id } = req.params;
const userId = (req as any).user.id;
const poll = await this.pollService.startPoll(id, userId);
res.json(poll);
} catch (error) {
console.error("Error starting poll:", error);
if (error instanceof Error) {
if (error.message === "Poll not found") {
return res.status(404).json({ error: error.message });
}
if (error.message === "Not authorized to start this poll") {
return res.status(403).json({ error: error.message });
}
if (error.message === "Only draft polls can be started") {
return res.status(400).json({ error: error.message });
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
res.status(500).json({ error: "Failed to start poll" });
}
};

endPoll = async (req: Request, res: Response) => {
try {
const { id } = req.params;
const userId = (req as any).user.id;
const poll = await this.pollService.endPoll(id, userId);
res.json(poll);
} catch (error) {
console.error("Error ending poll:", error);
if (error instanceof Error) {
if (error.message === "Poll not found") {
return res.status(404).json({ error: error.message });
}
if (error.message === "Not authorized to end this poll") {
return res.status(403).json({ error: error.message });
}
if (error.message === "Only active polls can be ended") {
return res.status(400).json({ error: error.message });
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
res.status(500).json({ error: "Failed to end poll" });
}
};
}
6 changes: 6 additions & 0 deletions platforms/evoting/api/src/database/entities/Poll.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -40,6 +40,12 @@ export class Poll {
@Column("simple-array")
options!: string[]; // stored as comma-separated values

@Column("enum", {
enum: ["draft", "active", "ended"],
default: "active",
})
status!: "draft" | "active" | "ended";

@Column({ type: "timestamp", nullable: true })
deadline!: Date | null;

Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
import { MigrationInterface, QueryRunner } from "typeorm";

export class Migration1772851200000 implements MigrationInterface {
name = 'Migration1772851200000'

public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`CREATE TYPE "public"."polls_status_enum" AS ENUM('draft', 'active', 'ended')`);
await queryRunner.query(`ALTER TABLE "polls" ADD "status" "public"."polls_status_enum" NOT NULL DEFAULT 'active'`);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "polls" DROP COLUMN "status"`);
await queryRunner.query(`DROP TYPE "public"."polls_status_enum"`);
}
}
2 changes: 2 additions & 0 deletions platforms/evoting/api/src/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -209,6 +209,8 @@ app.get("/api/polls/my", authGuard, pollController.getPollsByCreator);
app.post("/api/polls", authGuard, pollController.createPoll);
app.put("/api/polls/:id", authGuard, pollController.updatePoll);
app.delete("/api/polls/:id", authGuard, pollController.deletePoll);
app.post("/api/polls/:id/start", authGuard, pollController.startPoll);
app.post("/api/polls/:id/end", authGuard, pollController.endPoll);

// Vote routes
app.post("/api/votes", voteController.createVote.bind(voteController)); // Create normal/point/rank vote (old format)
Expand Down
4 changes: 2 additions & 2 deletions platforms/evoting/api/src/services/DeadlineCheckService.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -80,8 +80,8 @@ export class DeadlineCheckService {
voteId: poll.id
});

// Mark that we've sent the deadline message
await this.pollRepository.update(poll.id, { deadlineMessageSent: true });
// Mark that we've sent the deadline message and set status to ended
await this.pollRepository.update(poll.id, { deadlineMessageSent: true, status: "ended" });

console.log(`Successfully sent deadline message for poll: ${poll.title} (${poll.id})`);

Expand Down
73 changes: 68 additions & 5 deletions platforms/evoting/api/src/services/PollService.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -86,8 +86,8 @@ export class PollService {
// Custom sorting based on sortField and sortDirection
const sortedPolls = filteredPolls.sort((a, b) => {
const now = new Date();
const aIsActive = !a.deadline || new Date(a.deadline) > now;
const bIsActive = !b.deadline || new Date(b.deadline) > now;
const aIsActive = a.status === "active" || (a.status !== "ended" && a.status !== "draft" && (!a.deadline || new Date(a.deadline) > now));
const bIsActive = b.status === "active" || (b.status !== "ended" && b.status !== "draft" && (!b.deadline || new Date(b.deadline) > now));

// Apply the user's chosen sorting
let comparison = 0;
Expand DownExpand Up@@ -218,13 +218,15 @@ export class PollService {
throw new Error("Blind voting (private visibility) cannot be combined with eReputation weighted voting.");
}

const hasDeadline = !!pollData.deadline;
const pollDataForEntity = {
title: pollData.title,
mode: pollData.mode as "normal" | "point" | "rank",
visibility: pollData.visibility as "public" | "private",
votingWeight: votingWeight,
options: pollData.options,
deadline: pollData.deadline ? new Date(pollData.deadline) : null,
status: hasDeadline ? "active" as const : "draft" as const,
deadline: hasDeadline ? new Date(pollData.deadline!) : null,
creator,
creatorId: pollData.creatorId,
groupId: pollData.groupId || null
Expand All@@ -237,8 +239,8 @@ export class PollService {
const savedPoll = await this.pollRepository.save(poll);
console.log('🔍 Poll saved to database:', savedPoll);

// Create a system message about the new vote
if (pollData.groupId) {
// Create a system message about the new vote (only for active polls, not drafts)
if (pollData.groupId && hasDeadline) {
await this.messageService.createVoteCreatedMessage(pollData.groupId, pollData.title, savedPoll.id, creator.name, savedPoll.deadline);
}

Expand DownExpand Up@@ -301,6 +303,67 @@ export class PollService {



/**
* Manually start a poll (draft → active). Only creator can do this.
*/
async startPoll(id: string, userId: string): Promise<Poll> {
const poll = await this.getPollById(id);
if (!poll) {
throw new Error("Poll not found");
}
if (poll.creatorId !== userId) {
throw new Error("Not authorized to start this poll");
}
if (poll.status !== "draft") {
throw new Error("Only draft polls can be started");
}

await this.pollRepository.update(id, { status: "active" });

// Send system message that voting is now open
if (poll.groupId) {
await this.messageService.createVoteCreatedMessage(
poll.groupId,
poll.title,
poll.id,
poll.creator.name,
poll.deadline
);
}

return (await this.getPollById(id))!;
}

/**
* Manually end a poll (active → ended). Only creator can do this.
*/
async endPoll(id: string, userId: string): Promise<Poll> {
const poll = await this.getPollById(id);
if (!poll) {
throw new Error("Poll not found");
}
if (poll.creatorId !== userId) {
throw new Error("Not authorized to end this poll");
}
if (poll.status !== "active") {
throw new Error("Only active polls can be ended");
}

await this.pollRepository.update(id, { status: "ended", deadlineMessageSent: true });

// Send system message that voting has ended
if (poll.groupId) {
const voteUrl = `${process.env.PUBLIC_EVOTING_URL || 'http://localhost:3000'}/${poll.id}`;
await this.messageService.createSystemMessage({
text: `eVoting Platform: Vote has been manually closed!\n\n"${poll.title}"\n\nVote ID: ${poll.id}\n\nClosed by: ${poll.creator.name}\n\n<a href="${voteUrl}" target="_blank">View results here</a>`,
groupId: poll.groupId,
voteId: poll.id
});
}

return (await this.getPollById(id))!;
}

/**
* Get polls by group ID
*/
Expand Down
24 changes: 24 additions & 0 deletions platforms/evoting/api/src/services/VoteService.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -115,6 +115,14 @@ export class VoteService {
throw new Error('Poll not found');
}

// Prevent voting on polls that are not active
if (poll.status === "draft") {
throw new Error('Voting has not started yet');
}
if (poll.status === "ended") {
throw new Error('Voting has ended');
}

// Check if user has already voted
const existingVote = await this.voteRepository.findOne({
where: { pollId, userId }
Expand DownExpand Up@@ -178,6 +186,14 @@ export class VoteService {
throw new Error('Poll not found');
}

// Prevent voting on polls that are not active
if (poll.status === "draft") {
throw new Error('Voting has not started yet');
}
if (poll.status === "ended") {
throw new Error('Voting has ended');
}

const delegation = await this.delegationRepository.findOne({
where: {
pollId,
Expand DownExpand Up@@ -660,6 +676,14 @@ export class VoteService {
throw new Error('Poll not found');
}

// Prevent voting on polls that are not active
if (poll.status === "draft") {
throw new Error('Voting has not started yet');
}
if (poll.status === "ended") {
throw new Error('Voting has ended');
}

// Create election if it doesn't exist
try {
const options = poll.options.map((opt: string, index: number) => `option_${index}`);
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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
31 changes: 29 additions & 2 deletions platforms/evoting/api/scripts/seed.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -486,7 +486,33 @@ async function seed() {

console.log(`\n📊 Polls created: ${pollsCreated}, Votes created: ${votesCreated}\n`);

// Optionally add a custom user to all groups
// Auto-add all non-seed users (created via dev-sandbox auth/webhooks) to all groups
const seedEnames = new Set(seedUsers.map(u => u.ename));
const allUsers = await userRepo.find();
const nonSeedUsers = allUsers.filter(u => !seedEnames.has(u.ename));

if (nonSeedUsers.length > 0) {
console.log(`👤 Adding ${nonSeedUsers.length} existing user(s) to all groups...`);

const groupsToUpdate = await groupRepo.find({
relations: ["members", "participants"],
});

for (const user of nonSeedUsers) {
for (const group of groupsToUpdate) {
const isAlreadyMember = group.members.some((m) => m.id === user.id);

if (!isAlreadyMember) {
group.members.push(user);
group.participants.push(user);
await groupRepo.save(group);
console.log(` ✅ Added "${user.name || user.ename}" to "${group.name}"`);
}
}
}
}

// Also support explicit ename argument
const customEname = process.argv[2];
if (customEname) {
console.log(`\n👤 Adding custom user to all groups...`);
Expand DownExpand Up@@ -532,7 +558,8 @@ async function seed() {

console.log("\n🎉 Seed completed successfully!\n");
console.log("Summary:");
console.log(` - ${seedUsers.length} users`);
console.log(` - ${seedUsers.length} seed users`);
console.log(` - ${nonSeedUsers.length} existing user(s) added to all groups`);
console.log(` - ${seedGroups.length} groups (with charters)`);
console.log(` - ${seedPolls.length} polls (various modes: normal, point, rank)`);
console.log(` - Includes public and private polls with sample votes`);
Expand Down
46 changes: 46 additions & 0 deletions platforms/evoting/api/src/controllers/PollController.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -156,4 +156,50 @@ export class PollController {
res.status(500).json({ error: "Failed to fetch user polls" });
}
};

startPoll = async (req: Request, res: Response) => {
try {
const { id } = req.params;
const userId = (req as any).user.id;
const poll = await this.pollService.startPoll(id, userId);
res.json(poll);
} catch (error) {
console.error("Error starting poll:", error);
if (error instanceof Error) {
if (error.message === "Poll not found") {
return res.status(404).json({ error: error.message });
}
if (error.message === "Not authorized to start this poll") {
return res.status(403).json({ error: error.message });
}
if (error.message === "Only draft polls can be started") {
return res.status(400).json({ error: error.message });
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
res.status(500).json({ error: "Failed to start poll" });
}
};

endPoll = async (req: Request, res: Response) => {
try {
const { id } = req.params;
const userId = (req as any).user.id;
const poll = await this.pollService.endPoll(id, userId);
res.json(poll);
} catch (error) {
console.error("Error ending poll:", error);
if (error instanceof Error) {
if (error.message === "Poll not found") {
return res.status(404).json({ error: error.message });
}
if (error.message === "Not authorized to end this poll") {
return res.status(403).json({ error: error.message });
}
if (error.message === "Only active polls can be ended") {
return res.status(400).json({ error: error.message });
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
res.status(500).json({ error: "Failed to end poll" });
}
};
}
6 changes: 6 additions & 0 deletions platforms/evoting/api/src/database/entities/Poll.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -40,6 +40,12 @@ export class Poll {
@Column("simple-array")
options!: string[]; // stored as comma-separated values

@Column("enum", {
enum: ["draft", "active", "ended"],
default: "active",
})
status!: "draft" | "active" | "ended";

@Column({ type: "timestamp", nullable: true })
deadline!: Date | null;

Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
import { MigrationInterface, QueryRunner } from "typeorm";

export class Migration1772851200000 implements MigrationInterface {
name = 'Migration1772851200000'

public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`CREATE TYPE "public"."polls_status_enum" AS ENUM('draft', 'active', 'ended')`);
await queryRunner.query(`ALTER TABLE "polls" ADD "status" "public"."polls_status_enum" NOT NULL DEFAULT 'active'`);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "polls" DROP COLUMN "status"`);
await queryRunner.query(`DROP TYPE "public"."polls_status_enum"`);
}
}
2 changes: 2 additions & 0 deletions platforms/evoting/api/src/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -209,6 +209,8 @@ app.get("/api/polls/my", authGuard, pollController.getPollsByCreator);
app.post("/api/polls", authGuard, pollController.createPoll);
app.put("/api/polls/:id", authGuard, pollController.updatePoll);
app.delete("/api/polls/:id", authGuard, pollController.deletePoll);
app.post("/api/polls/:id/start", authGuard, pollController.startPoll);
app.post("/api/polls/:id/end", authGuard, pollController.endPoll);

// Vote routes
app.post("/api/votes", voteController.createVote.bind(voteController)); // Create normal/point/rank vote (old format)
Expand Down
4 changes: 2 additions & 2 deletions platforms/evoting/api/src/services/DeadlineCheckService.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -80,8 +80,8 @@ export class DeadlineCheckService {
voteId: poll.id
});

// Mark that we've sent the deadline message
await this.pollRepository.update(poll.id, { deadlineMessageSent: true });
// Mark that we've sent the deadline message and set status to ended
await this.pollRepository.update(poll.id, { deadlineMessageSent: true, status: "ended" });

console.log(`Successfully sent deadline message for poll: ${poll.title} (${poll.id})`);

Expand Down
73 changes: 68 additions & 5 deletions platforms/evoting/api/src/services/PollService.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -86,8 +86,8 @@ export class PollService {
// Custom sorting based on sortField and sortDirection
const sortedPolls = filteredPolls.sort((a, b) => {
const now = new Date();
const aIsActive = !a.deadline || new Date(a.deadline) > now;
const bIsActive = !b.deadline || new Date(b.deadline) > now;
const aIsActive = a.status === "active" || (a.status !== "ended" && a.status !== "draft" && (!a.deadline || new Date(a.deadline) > now));
const bIsActive = b.status === "active" || (b.status !== "ended" && b.status !== "draft" && (!b.deadline || new Date(b.deadline) > now));

// Apply the user's chosen sorting
let comparison = 0;
Expand DownExpand Up@@ -218,13 +218,15 @@ export class PollService {
throw new Error("Blind voting (private visibility) cannot be combined with eReputation weighted voting.");
}

const hasDeadline = !!pollData.deadline;
const pollDataForEntity = {
title: pollData.title,
mode: pollData.mode as "normal" | "point" | "rank",
visibility: pollData.visibility as "public" | "private",
votingWeight: votingWeight,
options: pollData.options,
deadline: pollData.deadline ? new Date(pollData.deadline) : null,
status: hasDeadline ? "active" as const : "draft" as const,
deadline: hasDeadline ? new Date(pollData.deadline!) : null,
creator,
creatorId: pollData.creatorId,
groupId: pollData.groupId || null
Expand All@@ -237,8 +239,8 @@ export class PollService {
const savedPoll = await this.pollRepository.save(poll);
console.log('🔍 Poll saved to database:', savedPoll);

// Create a system message about the new vote
if (pollData.groupId) {
// Create a system message about the new vote (only for active polls, not drafts)
if (pollData.groupId && hasDeadline) {
await this.messageService.createVoteCreatedMessage(pollData.groupId, pollData.title, savedPoll.id, creator.name, savedPoll.deadline);
}

Expand DownExpand Up@@ -301,6 +303,67 @@ export class PollService {



/**
* Manually start a poll (draft → active). Only creator can do this.
*/
async startPoll(id: string, userId: string): Promise<Poll> {
const poll = await this.getPollById(id);
if (!poll) {
throw new Error("Poll not found");
}
if (poll.creatorId !== userId) {
throw new Error("Not authorized to start this poll");
}
if (poll.status !== "draft") {
throw new Error("Only draft polls can be started");
}

await this.pollRepository.update(id, { status: "active" });

// Send system message that voting is now open
if (poll.groupId) {
await this.messageService.createVoteCreatedMessage(
poll.groupId,
poll.title,
poll.id,
poll.creator.name,
poll.deadline
);
}

return (await this.getPollById(id))!;
}

/**
* Manually end a poll (active → ended). Only creator can do this.
*/
async endPoll(id: string, userId: string): Promise<Poll> {
const poll = await this.getPollById(id);
if (!poll) {
throw new Error("Poll not found");
}
if (poll.creatorId !== userId) {
throw new Error("Not authorized to end this poll");
}
if (poll.status !== "active") {
throw new Error("Only active polls can be ended");
}

await this.pollRepository.update(id, { status: "ended", deadlineMessageSent: true });

// Send system message that voting has ended
if (poll.groupId) {
const voteUrl = `${process.env.PUBLIC_EVOTING_URL || 'http://localhost:3000'}/${poll.id}`;
await this.messageService.createSystemMessage({
text: `eVoting Platform: Vote has been manually closed!\n\n"${poll.title}"\n\nVote ID: ${poll.id}\n\nClosed by: ${poll.creator.name}\n\n<a href="${voteUrl}" target="_blank">View results here</a>`,
groupId: poll.groupId,
voteId: poll.id
});
}

return (await this.getPollById(id))!;
}

/**
* Get polls by group ID
*/
Expand Down
24 changes: 24 additions & 0 deletions platforms/evoting/api/src/services/VoteService.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -115,6 +115,14 @@ export class VoteService {
throw new Error('Poll not found');
}

// Prevent voting on polls that are not active
if (poll.status === "draft") {
throw new Error('Voting has not started yet');
}
if (poll.status === "ended") {
throw new Error('Voting has ended');
}

// Check if user has already voted
const existingVote = await this.voteRepository.findOne({
where: { pollId, userId }
Expand DownExpand Up@@ -178,6 +186,14 @@ export class VoteService {
throw new Error('Poll not found');
}

// Prevent voting on polls that are not active
if (poll.status === "draft") {
throw new Error('Voting has not started yet');
}
if (poll.status === "ended") {
throw new Error('Voting has ended');
}

const delegation = await this.delegationRepository.findOne({
where: {
pollId,
Expand DownExpand Up@@ -660,6 +676,14 @@ export class VoteService {
throw new Error('Poll not found');
}

// Prevent voting on polls that are not active
if (poll.status === "draft") {
throw new Error('Voting has not started yet');
}
if (poll.status === "ended") {
throw new Error('Voting has ended');
}

// Create election if it doesn't exist
try {
const options = poll.options.map((opt: string, index: number) => `option_${index}`);
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
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
31 changes: 29 additions & 2 deletions platforms/evoting/api/scripts/seed.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -486,7 +486,33 @@ async function seed() {

console.log(`\n📊 Polls created: ${pollsCreated}, Votes created: ${votesCreated}\n`);

// Optionally add a custom user to all groups
// Auto-add all non-seed users (created via dev-sandbox auth/webhooks) to all groups
const seedEnames = new Set(seedUsers.map(u => u.ename));
const allUsers = await userRepo.find();
const nonSeedUsers = allUsers.filter(u => !seedEnames.has(u.ename));

if (nonSeedUsers.length > 0) {
console.log(`👤 Adding ${nonSeedUsers.length} existing user(s) to all groups...`);

const groupsToUpdate = await groupRepo.find({
relations: ["members", "participants"],
});

for (const user of nonSeedUsers) {
for (const group of groupsToUpdate) {
const isAlreadyMember = group.members.some((m) => m.id === user.id);

if (!isAlreadyMember) {
group.members.push(user);
group.participants.push(user);
await groupRepo.save(group);
console.log(` ✅ Added "${user.name || user.ename}" to "${group.name}"`);
}
}
}
}

// Also support explicit ename argument
const customEname = process.argv[2];
if (customEname) {
console.log(`\n👤 Adding custom user to all groups...`);
Expand DownExpand Up@@ -532,7 +558,8 @@ async function seed() {

console.log("\n🎉 Seed completed successfully!\n");
console.log("Summary:");
console.log(` - ${seedUsers.length} users`);
console.log(` - ${seedUsers.length} seed users`);
console.log(` - ${nonSeedUsers.length} existing user(s) added to all groups`);
console.log(` - ${seedGroups.length} groups (with charters)`);
console.log(` - ${seedPolls.length} polls (various modes: normal, point, rank)`);
console.log(` - Includes public and private polls with sample votes`);
Expand Down
46 changes: 46 additions & 0 deletions platforms/evoting/api/src/controllers/PollController.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -156,4 +156,50 @@ export class PollController {
res.status(500).json({ error: "Failed to fetch user polls" });
}
};

startPoll = async (req: Request, res: Response) => {
try {
const { id } = req.params;
const userId = (req as any).user.id;
const poll = await this.pollService.startPoll(id, userId);
res.json(poll);
} catch (error) {
console.error("Error starting poll:", error);
if (error instanceof Error) {
if (error.message === "Poll not found") {
return res.status(404).json({ error: error.message });
}
if (error.message === "Not authorized to start this poll") {
return res.status(403).json({ error: error.message });
}
if (error.message === "Only draft polls can be started") {
return res.status(400).json({ error: error.message });
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
res.status(500).json({ error: "Failed to start poll" });
}
};

endPoll = async (req: Request, res: Response) => {
try {
const { id } = req.params;
const userId = (req as any).user.id;
const poll = await this.pollService.endPoll(id, userId);
res.json(poll);
} catch (error) {
console.error("Error ending poll:", error);
if (error instanceof Error) {
if (error.message === "Poll not found") {
return res.status(404).json({ error: error.message });
}
if (error.message === "Not authorized to end this poll") {
return res.status(403).json({ error: error.message });
}
if (error.message === "Only active polls can be ended") {
return res.status(400).json({ error: error.message });
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
res.status(500).json({ error: "Failed to end poll" });
}
};
}
6 changes: 6 additions & 0 deletions platforms/evoting/api/src/database/entities/Poll.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -40,6 +40,12 @@ export class Poll {
@Column("simple-array")
options!: string[]; // stored as comma-separated values

@Column("enum", {
enum: ["draft", "active", "ended"],
default: "active",
})
status!: "draft" | "active" | "ended";

@Column({ type: "timestamp", nullable: true })
deadline!: Date | null;

Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
import { MigrationInterface, QueryRunner } from "typeorm";

export class Migration1772851200000 implements MigrationInterface {
name = 'Migration1772851200000'

public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`CREATE TYPE "public"."polls_status_enum" AS ENUM('draft', 'active', 'ended')`);
await queryRunner.query(`ALTER TABLE "polls" ADD "status" "public"."polls_status_enum" NOT NULL DEFAULT 'active'`);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "polls" DROP COLUMN "status"`);
await queryRunner.query(`DROP TYPE "public"."polls_status_enum"`);
}
}
2 changes: 2 additions & 0 deletions platforms/evoting/api/src/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -209,6 +209,8 @@ app.get("/api/polls/my", authGuard, pollController.getPollsByCreator);
app.post("/api/polls", authGuard, pollController.createPoll);
app.put("/api/polls/:id", authGuard, pollController.updatePoll);
app.delete("/api/polls/:id", authGuard, pollController.deletePoll);
app.post("/api/polls/:id/start", authGuard, pollController.startPoll);
app.post("/api/polls/:id/end", authGuard, pollController.endPoll);

// Vote routes
app.post("/api/votes", voteController.createVote.bind(voteController)); // Create normal/point/rank vote (old format)
Expand Down
4 changes: 2 additions & 2 deletions platforms/evoting/api/src/services/DeadlineCheckService.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -80,8 +80,8 @@ export class DeadlineCheckService {
voteId: poll.id
});

// Mark that we've sent the deadline message
await this.pollRepository.update(poll.id, { deadlineMessageSent: true });
// Mark that we've sent the deadline message and set status to ended
await this.pollRepository.update(poll.id, { deadlineMessageSent: true, status: "ended" });

console.log(`Successfully sent deadline message for poll: ${poll.title} (${poll.id})`);

Expand Down
73 changes: 68 additions & 5 deletions platforms/evoting/api/src/services/PollService.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -86,8 +86,8 @@ export class PollService {
// Custom sorting based on sortField and sortDirection
const sortedPolls = filteredPolls.sort((a, b) => {
const now = new Date();
const aIsActive = !a.deadline || new Date(a.deadline) > now;
const bIsActive = !b.deadline || new Date(b.deadline) > now;
const aIsActive = a.status === "active" || (a.status !== "ended" && a.status !== "draft" && (!a.deadline || new Date(a.deadline) > now));
const bIsActive = b.status === "active" || (b.status !== "ended" && b.status !== "draft" && (!b.deadline || new Date(b.deadline) > now));

// Apply the user's chosen sorting
let comparison = 0;
Expand DownExpand Up@@ -218,13 +218,15 @@ export class PollService {
throw new Error("Blind voting (private visibility) cannot be combined with eReputation weighted voting.");
}

const hasDeadline = !!pollData.deadline;
const pollDataForEntity = {
title: pollData.title,
mode: pollData.mode as "normal" | "point" | "rank",
visibility: pollData.visibility as "public" | "private",
votingWeight: votingWeight,
options: pollData.options,
deadline: pollData.deadline ? new Date(pollData.deadline) : null,
status: hasDeadline ? "active" as const : "draft" as const,
deadline: hasDeadline ? new Date(pollData.deadline!) : null,
creator,
creatorId: pollData.creatorId,
groupId: pollData.groupId || null
Expand All@@ -237,8 +239,8 @@ export class PollService {
const savedPoll = await this.pollRepository.save(poll);
console.log('🔍 Poll saved to database:', savedPoll);

// Create a system message about the new vote
if (pollData.groupId) {
// Create a system message about the new vote (only for active polls, not drafts)
if (pollData.groupId && hasDeadline) {
await this.messageService.createVoteCreatedMessage(pollData.groupId, pollData.title, savedPoll.id, creator.name, savedPoll.deadline);
}

Expand DownExpand Up@@ -301,6 +303,67 @@ export class PollService {



/**
* Manually start a poll (draft → active). Only creator can do this.
*/
async startPoll(id: string, userId: string): Promise<Poll> {
const poll = await this.getPollById(id);
if (!poll) {
throw new Error("Poll not found");
}
if (poll.creatorId !== userId) {
throw new Error("Not authorized to start this poll");
}
if (poll.status !== "draft") {
throw new Error("Only draft polls can be started");
}

await this.pollRepository.update(id, { status: "active" });

// Send system message that voting is now open
if (poll.groupId) {
await this.messageService.createVoteCreatedMessage(
poll.groupId,
poll.title,
poll.id,
poll.creator.name,
poll.deadline
);
}

return (await this.getPollById(id))!;
}

/**
* Manually end a poll (active → ended). Only creator can do this.
*/
async endPoll(id: string, userId: string): Promise<Poll> {
const poll = await this.getPollById(id);
if (!poll) {
throw new Error("Poll not found");
}
if (poll.creatorId !== userId) {
throw new Error("Not authorized to end this poll");
}
if (poll.status !== "active") {
throw new Error("Only active polls can be ended");
}

await this.pollRepository.update(id, { status: "ended", deadlineMessageSent: true });

// Send system message that voting has ended
if (poll.groupId) {
const voteUrl = `${process.env.PUBLIC_EVOTING_URL || 'http://localhost:3000'}/${poll.id}`;
await this.messageService.createSystemMessage({
text: `eVoting Platform: Vote has been manually closed!\n\n"${poll.title}"\n\nVote ID: ${poll.id}\n\nClosed by: ${poll.creator.name}\n\n<a href="${voteUrl}" target="_blank">View results here</a>`,
groupId: poll.groupId,
voteId: poll.id
});
}

return (await this.getPollById(id))!;
}

/**
* Get polls by group ID
*/
Expand Down
24 changes: 24 additions & 0 deletions platforms/evoting/api/src/services/VoteService.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -115,6 +115,14 @@ export class VoteService {
throw new Error('Poll not found');
}

// Prevent voting on polls that are not active
if (poll.status === "draft") {
throw new Error('Voting has not started yet');
}
if (poll.status === "ended") {
throw new Error('Voting has ended');
}

// Check if user has already voted
const existingVote = await this.voteRepository.findOne({
where: { pollId, userId }
Expand DownExpand Up@@ -178,6 +186,14 @@ export class VoteService {
throw new Error('Poll not found');
}

// Prevent voting on polls that are not active
if (poll.status === "draft") {
throw new Error('Voting has not started yet');
}
if (poll.status === "ended") {
throw new Error('Voting has ended');
}

const delegation = await this.delegationRepository.findOne({
where: {
pollId,
Expand DownExpand Up@@ -660,6 +676,14 @@ export class VoteService {
throw new Error('Poll not found');
}

// Prevent voting on polls that are not active
if (poll.status === "draft") {
throw new Error('Voting has not started yet');
}
if (poll.status === "ended") {
throw new Error('Voting has ended');
}

// Create election if it doesn't exist
try {
const options = poll.options.map((opt: string, index: number) => `option_${index}`);
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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
31 changes: 29 additions & 2 deletions platforms/evoting/api/scripts/seed.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -486,7 +486,33 @@ async function seed() {

console.log(`\n📊 Polls created: ${pollsCreated}, Votes created: ${votesCreated}\n`);

// Optionally add a custom user to all groups
// Auto-add all non-seed users (created via dev-sandbox auth/webhooks) to all groups
const seedEnames = new Set(seedUsers.map(u => u.ename));
const allUsers = await userRepo.find();
const nonSeedUsers = allUsers.filter(u => !seedEnames.has(u.ename));

if (nonSeedUsers.length > 0) {
console.log(`👤 Adding ${nonSeedUsers.length} existing user(s) to all groups...`);

const groupsToUpdate = await groupRepo.find({
relations: ["members", "participants"],
});

for (const user of nonSeedUsers) {
for (const group of groupsToUpdate) {
const isAlreadyMember = group.members.some((m) => m.id === user.id);

if (!isAlreadyMember) {
group.members.push(user);
group.participants.push(user);
await groupRepo.save(group);
console.log(` ✅ Added "${user.name || user.ename}" to "${group.name}"`);
}
}
}
}

// Also support explicit ename argument
const customEname = process.argv[2];
if (customEname) {
console.log(`\n👤 Adding custom user to all groups...`);
Expand DownExpand Up@@ -532,7 +558,8 @@ async function seed() {

console.log("\n🎉 Seed completed successfully!\n");
console.log("Summary:");
console.log(` - ${seedUsers.length} users`);
console.log(` - ${seedUsers.length} seed users`);
console.log(` - ${nonSeedUsers.length} existing user(s) added to all groups`);
console.log(` - ${seedGroups.length} groups (with charters)`);
console.log(` - ${seedPolls.length} polls (various modes: normal, point, rank)`);
console.log(` - Includes public and private polls with sample votes`);
Expand Down
46 changes: 46 additions & 0 deletions platforms/evoting/api/src/controllers/PollController.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -156,4 +156,50 @@ export class PollController {
res.status(500).json({ error: "Failed to fetch user polls" });
}
};

startPoll = async (req: Request, res: Response) => {
try {
const { id } = req.params;
const userId = (req as any).user.id;
const poll = await this.pollService.startPoll(id, userId);
res.json(poll);
} catch (error) {
console.error("Error starting poll:", error);
if (error instanceof Error) {
if (error.message === "Poll not found") {
return res.status(404).json({ error: error.message });
}
if (error.message === "Not authorized to start this poll") {
return res.status(403).json({ error: error.message });
}
if (error.message === "Only draft polls can be started") {
return res.status(400).json({ error: error.message });
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
res.status(500).json({ error: "Failed to start poll" });
}
};

endPoll = async (req: Request, res: Response) => {
try {
const { id } = req.params;
const userId = (req as any).user.id;
const poll = await this.pollService.endPoll(id, userId);
res.json(poll);
} catch (error) {
console.error("Error ending poll:", error);
if (error instanceof Error) {
if (error.message === "Poll not found") {
return res.status(404).json({ error: error.message });
}
if (error.message === "Not authorized to end this poll") {
return res.status(403).json({ error: error.message });
}
if (error.message === "Only active polls can be ended") {
return res.status(400).json({ error: error.message });
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
res.status(500).json({ error: "Failed to end poll" });
}
};
}
6 changes: 6 additions & 0 deletions platforms/evoting/api/src/database/entities/Poll.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -40,6 +40,12 @@ export class Poll {
@Column("simple-array")
options!: string[]; // stored as comma-separated values

@Column("enum", {
enum: ["draft", "active", "ended"],
default: "active",
})
status!: "draft" | "active" | "ended";

@Column({ type: "timestamp", nullable: true })
deadline!: Date | null;

Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
import { MigrationInterface, QueryRunner } from "typeorm";

export class Migration1772851200000 implements MigrationInterface {
name = 'Migration1772851200000'

public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`CREATE TYPE "public"."polls_status_enum" AS ENUM('draft', 'active', 'ended')`);
await queryRunner.query(`ALTER TABLE "polls" ADD "status" "public"."polls_status_enum" NOT NULL DEFAULT 'active'`);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "polls" DROP COLUMN "status"`);
await queryRunner.query(`DROP TYPE "public"."polls_status_enum"`);
}
}
2 changes: 2 additions & 0 deletions platforms/evoting/api/src/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -209,6 +209,8 @@ app.get("/api/polls/my", authGuard, pollController.getPollsByCreator);
app.post("/api/polls", authGuard, pollController.createPoll);
app.put("/api/polls/:id", authGuard, pollController.updatePoll);
app.delete("/api/polls/:id", authGuard, pollController.deletePoll);
app.post("/api/polls/:id/start", authGuard, pollController.startPoll);
app.post("/api/polls/:id/end", authGuard, pollController.endPoll);

// Vote routes
app.post("/api/votes", voteController.createVote.bind(voteController)); // Create normal/point/rank vote (old format)
Expand Down
4 changes: 2 additions & 2 deletions platforms/evoting/api/src/services/DeadlineCheckService.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -80,8 +80,8 @@ export class DeadlineCheckService {
voteId: poll.id
});

// Mark that we've sent the deadline message
await this.pollRepository.update(poll.id, { deadlineMessageSent: true });
// Mark that we've sent the deadline message and set status to ended
await this.pollRepository.update(poll.id, { deadlineMessageSent: true, status: "ended" });

console.log(`Successfully sent deadline message for poll: ${poll.title} (${poll.id})`);

Expand Down
73 changes: 68 additions & 5 deletions platforms/evoting/api/src/services/PollService.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -86,8 +86,8 @@ export class PollService {
// Custom sorting based on sortField and sortDirection
const sortedPolls = filteredPolls.sort((a, b) => {
const now = new Date();
const aIsActive = !a.deadline || new Date(a.deadline) > now;
const bIsActive = !b.deadline || new Date(b.deadline) > now;
const aIsActive = a.status === "active" || (a.status !== "ended" && a.status !== "draft" && (!a.deadline || new Date(a.deadline) > now));
const bIsActive = b.status === "active" || (b.status !== "ended" && b.status !== "draft" && (!b.deadline || new Date(b.deadline) > now));

// Apply the user's chosen sorting
let comparison = 0;
Expand DownExpand Up@@ -218,13 +218,15 @@ export class PollService {
throw new Error("Blind voting (private visibility) cannot be combined with eReputation weighted voting.");
}

const hasDeadline = !!pollData.deadline;
const pollDataForEntity = {
title: pollData.title,
mode: pollData.mode as "normal" | "point" | "rank",
visibility: pollData.visibility as "public" | "private",
votingWeight: votingWeight,
options: pollData.options,
deadline: pollData.deadline ? new Date(pollData.deadline) : null,
status: hasDeadline ? "active" as const : "draft" as const,
deadline: hasDeadline ? new Date(pollData.deadline!) : null,
creator,
creatorId: pollData.creatorId,
groupId: pollData.groupId || null
Expand All@@ -237,8 +239,8 @@ export class PollService {
const savedPoll = await this.pollRepository.save(poll);
console.log('🔍 Poll saved to database:', savedPoll);

// Create a system message about the new vote
if (pollData.groupId) {
// Create a system message about the new vote (only for active polls, not drafts)
if (pollData.groupId && hasDeadline) {
await this.messageService.createVoteCreatedMessage(pollData.groupId, pollData.title, savedPoll.id, creator.name, savedPoll.deadline);
}

Expand DownExpand Up@@ -301,6 +303,67 @@ export class PollService {



/**
* Manually start a poll (draft → active). Only creator can do this.
*/
async startPoll(id: string, userId: string): Promise<Poll> {
const poll = await this.getPollById(id);
if (!poll) {
throw new Error("Poll not found");
}
if (poll.creatorId !== userId) {
throw new Error("Not authorized to start this poll");
}
if (poll.status !== "draft") {
throw new Error("Only draft polls can be started");
}

await this.pollRepository.update(id, { status: "active" });

// Send system message that voting is now open
if (poll.groupId) {
await this.messageService.createVoteCreatedMessage(
poll.groupId,
poll.title,
poll.id,
poll.creator.name,
poll.deadline
);
}

return (await this.getPollById(id))!;
}

/**
* Manually end a poll (active → ended). Only creator can do this.
*/
async endPoll(id: string, userId: string): Promise<Poll> {
const poll = await this.getPollById(id);
if (!poll) {
throw new Error("Poll not found");
}
if (poll.creatorId !== userId) {
throw new Error("Not authorized to end this poll");
}
if (poll.status !== "active") {
throw new Error("Only active polls can be ended");
}

await this.pollRepository.update(id, { status: "ended", deadlineMessageSent: true });

// Send system message that voting has ended
if (poll.groupId) {
const voteUrl = `${process.env.PUBLIC_EVOTING_URL || 'http://localhost:3000'}/${poll.id}`;
await this.messageService.createSystemMessage({
text: `eVoting Platform: Vote has been manually closed!\n\n"${poll.title}"\n\nVote ID: ${poll.id}\n\nClosed by: ${poll.creator.name}\n\n<a href="${voteUrl}" target="_blank">View results here</a>`,
groupId: poll.groupId,
voteId: poll.id
});
}

return (await this.getPollById(id))!;
}

/**
* Get polls by group ID
*/
Expand Down
24 changes: 24 additions & 0 deletions platforms/evoting/api/src/services/VoteService.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -115,6 +115,14 @@ export class VoteService {
throw new Error('Poll not found');
}

// Prevent voting on polls that are not active
if (poll.status === "draft") {
throw new Error('Voting has not started yet');
}
if (poll.status === "ended") {
throw new Error('Voting has ended');
}

// Check if user has already voted
const existingVote = await this.voteRepository.findOne({
where: { pollId, userId }
Expand DownExpand Up@@ -178,6 +186,14 @@ export class VoteService {
throw new Error('Poll not found');
}

// Prevent voting on polls that are not active
if (poll.status === "draft") {
throw new Error('Voting has not started yet');
}
if (poll.status === "ended") {
throw new Error('Voting has ended');
}

const delegation = await this.delegationRepository.findOne({
where: {
pollId,
Expand DownExpand Up@@ -660,6 +676,14 @@ export class VoteService {
throw new Error('Poll not found');
}

// Prevent voting on polls that are not active
if (poll.status === "draft") {
throw new Error('Voting has not started yet');
}
if (poll.status === "ended") {
throw new Error('Voting has ended');
}

// Create election if it doesn't exist
try {
const options = poll.options.map((opt: string, index: number) => `option_${index}`);
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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
31 changes: 29 additions & 2 deletions platforms/evoting/api/scripts/seed.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -486,7 +486,33 @@ async function seed() {

console.log(`\n📊 Polls created: ${pollsCreated}, Votes created: ${votesCreated}\n`);

// Optionally add a custom user to all groups
// Auto-add all non-seed users (created via dev-sandbox auth/webhooks) to all groups
const seedEnames = new Set(seedUsers.map(u => u.ename));
const allUsers = await userRepo.find();
const nonSeedUsers = allUsers.filter(u => !seedEnames.has(u.ename));

if (nonSeedUsers.length > 0) {
console.log(`👤 Adding ${nonSeedUsers.length} existing user(s) to all groups...`);

const groupsToUpdate = await groupRepo.find({
relations: ["members", "participants"],
});

for (const user of nonSeedUsers) {
for (const group of groupsToUpdate) {
const isAlreadyMember = group.members.some((m) => m.id === user.id);

if (!isAlreadyMember) {
group.members.push(user);
group.participants.push(user);
await groupRepo.save(group);
console.log(` ✅ Added "${user.name || user.ename}" to "${group.name}"`);
}
}
}
}

// Also support explicit ename argument
const customEname = process.argv[2];
if (customEname) {
console.log(`\n👤 Adding custom user to all groups...`);
Expand DownExpand Up@@ -532,7 +558,8 @@ async function seed() {

console.log("\n🎉 Seed completed successfully!\n");
console.log("Summary:");
console.log(` - ${seedUsers.length} users`);
console.log(` - ${seedUsers.length} seed users`);
console.log(` - ${nonSeedUsers.length} existing user(s) added to all groups`);
console.log(` - ${seedGroups.length} groups (with charters)`);
console.log(` - ${seedPolls.length} polls (various modes: normal, point, rank)`);
console.log(` - Includes public and private polls with sample votes`);
Expand Down
46 changes: 46 additions & 0 deletions platforms/evoting/api/src/controllers/PollController.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -156,4 +156,50 @@ export class PollController {
res.status(500).json({ error: "Failed to fetch user polls" });
}
};

startPoll = async (req: Request, res: Response) => {
try {
const { id } = req.params;
const userId = (req as any).user.id;
const poll = await this.pollService.startPoll(id, userId);
res.json(poll);
} catch (error) {
console.error("Error starting poll:", error);
if (error instanceof Error) {
if (error.message === "Poll not found") {
return res.status(404).json({ error: error.message });
}
if (error.message === "Not authorized to start this poll") {
return res.status(403).json({ error: error.message });
}
if (error.message === "Only draft polls can be started") {
return res.status(400).json({ error: error.message });
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
res.status(500).json({ error: "Failed to start poll" });
}
};

endPoll = async (req: Request, res: Response) => {
try {
const { id } = req.params;
const userId = (req as any).user.id;
const poll = await this.pollService.endPoll(id, userId);
res.json(poll);
} catch (error) {
console.error("Error ending poll:", error);
if (error instanceof Error) {
if (error.message === "Poll not found") {
return res.status(404).json({ error: error.message });
}
if (error.message === "Not authorized to end this poll") {
return res.status(403).json({ error: error.message });
}
if (error.message === "Only active polls can be ended") {
return res.status(400).json({ error: error.message });
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
res.status(500).json({ error: "Failed to end poll" });
}
};
}
6 changes: 6 additions & 0 deletions platforms/evoting/api/src/database/entities/Poll.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -40,6 +40,12 @@ export class Poll {
@Column("simple-array")
options!: string[]; // stored as comma-separated values

@Column("enum", {
enum: ["draft", "active", "ended"],
default: "active",
})
status!: "draft" | "active" | "ended";

@Column({ type: "timestamp", nullable: true })
deadline!: Date | null;

Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
import { MigrationInterface, QueryRunner } from "typeorm";

export class Migration1772851200000 implements MigrationInterface {
name = 'Migration1772851200000'

public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`CREATE TYPE "public"."polls_status_enum" AS ENUM('draft', 'active', 'ended')`);
await queryRunner.query(`ALTER TABLE "polls" ADD "status" "public"."polls_status_enum" NOT NULL DEFAULT 'active'`);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "polls" DROP COLUMN "status"`);
await queryRunner.query(`DROP TYPE "public"."polls_status_enum"`);
}
}
2 changes: 2 additions & 0 deletions platforms/evoting/api/src/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -209,6 +209,8 @@ app.get("/api/polls/my", authGuard, pollController.getPollsByCreator);
app.post("/api/polls", authGuard, pollController.createPoll);
app.put("/api/polls/:id", authGuard, pollController.updatePoll);
app.delete("/api/polls/:id", authGuard, pollController.deletePoll);
app.post("/api/polls/:id/start", authGuard, pollController.startPoll);
app.post("/api/polls/:id/end", authGuard, pollController.endPoll);

// Vote routes
app.post("/api/votes", voteController.createVote.bind(voteController)); // Create normal/point/rank vote (old format)
Expand Down
4 changes: 2 additions & 2 deletions platforms/evoting/api/src/services/DeadlineCheckService.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -80,8 +80,8 @@ export class DeadlineCheckService {
voteId: poll.id
});

// Mark that we've sent the deadline message
await this.pollRepository.update(poll.id, { deadlineMessageSent: true });
// Mark that we've sent the deadline message and set status to ended
await this.pollRepository.update(poll.id, { deadlineMessageSent: true, status: "ended" });

console.log(`Successfully sent deadline message for poll: ${poll.title} (${poll.id})`);

Expand Down
73 changes: 68 additions & 5 deletions platforms/evoting/api/src/services/PollService.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -86,8 +86,8 @@ export class PollService {
// Custom sorting based on sortField and sortDirection
const sortedPolls = filteredPolls.sort((a, b) => {
const now = new Date();
const aIsActive = !a.deadline || new Date(a.deadline) > now;
const bIsActive = !b.deadline || new Date(b.deadline) > now;
const aIsActive = a.status === "active" || (a.status !== "ended" && a.status !== "draft" && (!a.deadline || new Date(a.deadline) > now));
const bIsActive = b.status === "active" || (b.status !== "ended" && b.status !== "draft" && (!b.deadline || new Date(b.deadline) > now));

// Apply the user's chosen sorting
let comparison = 0;
Expand DownExpand Up@@ -218,13 +218,15 @@ export class PollService {
throw new Error("Blind voting (private visibility) cannot be combined with eReputation weighted voting.");
}

const hasDeadline = !!pollData.deadline;
const pollDataForEntity = {
title: pollData.title,
mode: pollData.mode as "normal" | "point" | "rank",
visibility: pollData.visibility as "public" | "private",
votingWeight: votingWeight,
options: pollData.options,
deadline: pollData.deadline ? new Date(pollData.deadline) : null,
status: hasDeadline ? "active" as const : "draft" as const,
deadline: hasDeadline ? new Date(pollData.deadline!) : null,
creator,
creatorId: pollData.creatorId,
groupId: pollData.groupId || null
Expand All@@ -237,8 +239,8 @@ export class PollService {
const savedPoll = await this.pollRepository.save(poll);
console.log('🔍 Poll saved to database:', savedPoll);

// Create a system message about the new vote
if (pollData.groupId) {
// Create a system message about the new vote (only for active polls, not drafts)
if (pollData.groupId && hasDeadline) {
await this.messageService.createVoteCreatedMessage(pollData.groupId, pollData.title, savedPoll.id, creator.name, savedPoll.deadline);
}

Expand DownExpand Up@@ -301,6 +303,67 @@ export class PollService {



/**
* Manually start a poll (draft → active). Only creator can do this.
*/
async startPoll(id: string, userId: string): Promise<Poll> {
const poll = await this.getPollById(id);
if (!poll) {
throw new Error("Poll not found");
}
if (poll.creatorId !== userId) {
throw new Error("Not authorized to start this poll");
}
if (poll.status !== "draft") {
throw new Error("Only draft polls can be started");
}

await this.pollRepository.update(id, { status: "active" });

// Send system message that voting is now open
if (poll.groupId) {
await this.messageService.createVoteCreatedMessage(
poll.groupId,
poll.title,
poll.id,
poll.creator.name,
poll.deadline
);
}

return (await this.getPollById(id))!;
}

/**
* Manually end a poll (active → ended). Only creator can do this.
*/
async endPoll(id: string, userId: string): Promise<Poll> {
const poll = await this.getPollById(id);
if (!poll) {
throw new Error("Poll not found");
}
if (poll.creatorId !== userId) {
throw new Error("Not authorized to end this poll");
}
if (poll.status !== "active") {
throw new Error("Only active polls can be ended");
}

await this.pollRepository.update(id, { status: "ended", deadlineMessageSent: true });

// Send system message that voting has ended
if (poll.groupId) {
const voteUrl = `${process.env.PUBLIC_EVOTING_URL || 'http://localhost:3000'}/${poll.id}`;
await this.messageService.createSystemMessage({
text: `eVoting Platform: Vote has been manually closed!\n\n"${poll.title}"\n\nVote ID: ${poll.id}\n\nClosed by: ${poll.creator.name}\n\n<a href="${voteUrl}" target="_blank">View results here</a>`,
groupId: poll.groupId,
voteId: poll.id
});
}

return (await this.getPollById(id))!;
}

/**
* Get polls by group ID
*/
Expand Down
24 changes: 24 additions & 0 deletions platforms/evoting/api/src/services/VoteService.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -115,6 +115,14 @@ export class VoteService {
throw new Error('Poll not found');
}

// Prevent voting on polls that are not active
if (poll.status === "draft") {
throw new Error('Voting has not started yet');
}
if (poll.status === "ended") {
throw new Error('Voting has ended');
}

// Check if user has already voted
const existingVote = await this.voteRepository.findOne({
where: { pollId, userId }
Expand DownExpand Up@@ -178,6 +186,14 @@ export class VoteService {
throw new Error('Poll not found');
}

// Prevent voting on polls that are not active
if (poll.status === "draft") {
throw new Error('Voting has not started yet');
}
if (poll.status === "ended") {
throw new Error('Voting has ended');
}

const delegation = await this.delegationRepository.findOne({
where: {
pollId,
Expand DownExpand Up@@ -660,6 +676,14 @@ export class VoteService {
throw new Error('Poll not found');
}

// Prevent voting on polls that are not active
if (poll.status === "draft") {
throw new Error('Voting has not started yet');
}
if (poll.status === "ended") {
throw new Error('Voting has ended');
}

// Create election if it doesn't exist
try {
const options = poll.options.map((opt: string, index: number) => `option_${index}`);
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
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
31 changes: 29 additions & 2 deletions platforms/evoting/api/scripts/seed.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -486,7 +486,33 @@ async function seed() {

console.log(`\n📊 Polls created: ${pollsCreated}, Votes created: ${votesCreated}\n`);

// Optionally add a custom user to all groups
// Auto-add all non-seed users (created via dev-sandbox auth/webhooks) to all groups
const seedEnames = new Set(seedUsers.map(u => u.ename));
const allUsers = await userRepo.find();
const nonSeedUsers = allUsers.filter(u => !seedEnames.has(u.ename));

if (nonSeedUsers.length > 0) {
console.log(`👤 Adding ${nonSeedUsers.length} existing user(s) to all groups...`);

const groupsToUpdate = await groupRepo.find({
relations: ["members", "participants"],
});

for (const user of nonSeedUsers) {
for (const group of groupsToUpdate) {
const isAlreadyMember = group.members.some((m) => m.id === user.id);

if (!isAlreadyMember) {
group.members.push(user);
group.participants.push(user);
await groupRepo.save(group);
console.log(` ✅ Added "${user.name || user.ename}" to "${group.name}"`);
}
}
}
}

// Also support explicit ename argument
const customEname = process.argv[2];
if (customEname) {
console.log(`\n👤 Adding custom user to all groups...`);
Expand DownExpand Up@@ -532,7 +558,8 @@ async function seed() {

console.log("\n🎉 Seed completed successfully!\n");
console.log("Summary:");
console.log(` - ${seedUsers.length} users`);
console.log(` - ${seedUsers.length} seed users`);
console.log(` - ${nonSeedUsers.length} existing user(s) added to all groups`);
console.log(` - ${seedGroups.length} groups (with charters)`);
console.log(` - ${seedPolls.length} polls (various modes: normal, point, rank)`);
console.log(` - Includes public and private polls with sample votes`);
Expand Down
46 changes: 46 additions & 0 deletions platforms/evoting/api/src/controllers/PollController.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -156,4 +156,50 @@ export class PollController {
res.status(500).json({ error: "Failed to fetch user polls" });
}
};

startPoll = async (req: Request, res: Response) => {
try {
const { id } = req.params;
const userId = (req as any).user.id;
const poll = await this.pollService.startPoll(id, userId);
res.json(poll);
} catch (error) {
console.error("Error starting poll:", error);
if (error instanceof Error) {
if (error.message === "Poll not found") {
return res.status(404).json({ error: error.message });
}
if (error.message === "Not authorized to start this poll") {
return res.status(403).json({ error: error.message });
}
if (error.message === "Only draft polls can be started") {
return res.status(400).json({ error: error.message });
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
res.status(500).json({ error: "Failed to start poll" });
}
};

endPoll = async (req: Request, res: Response) => {
try {
const { id } = req.params;
const userId = (req as any).user.id;
const poll = await this.pollService.endPoll(id, userId);
res.json(poll);
} catch (error) {
console.error("Error ending poll:", error);
if (error instanceof Error) {
if (error.message === "Poll not found") {
return res.status(404).json({ error: error.message });
}
if (error.message === "Not authorized to end this poll") {
return res.status(403).json({ error: error.message });
}
if (error.message === "Only active polls can be ended") {
return res.status(400).json({ error: error.message });
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
res.status(500).json({ error: "Failed to end poll" });
}
};
}
6 changes: 6 additions & 0 deletions platforms/evoting/api/src/database/entities/Poll.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -40,6 +40,12 @@ export class Poll {
@Column("simple-array")
options!: string[]; // stored as comma-separated values

@Column("enum", {
enum: ["draft", "active", "ended"],
default: "active",
})
status!: "draft" | "active" | "ended";

@Column({ type: "timestamp", nullable: true })
deadline!: Date | null;

Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
import { MigrationInterface, QueryRunner } from "typeorm";

export class Migration1772851200000 implements MigrationInterface {
name = 'Migration1772851200000'

public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`CREATE TYPE "public"."polls_status_enum" AS ENUM('draft', 'active', 'ended')`);
await queryRunner.query(`ALTER TABLE "polls" ADD "status" "public"."polls_status_enum" NOT NULL DEFAULT 'active'`);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "polls" DROP COLUMN "status"`);
await queryRunner.query(`DROP TYPE "public"."polls_status_enum"`);
}
}
2 changes: 2 additions & 0 deletions platforms/evoting/api/src/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -209,6 +209,8 @@ app.get("/api/polls/my", authGuard, pollController.getPollsByCreator);
app.post("/api/polls", authGuard, pollController.createPoll);
app.put("/api/polls/:id", authGuard, pollController.updatePoll);
app.delete("/api/polls/:id", authGuard, pollController.deletePoll);
app.post("/api/polls/:id/start", authGuard, pollController.startPoll);
app.post("/api/polls/:id/end", authGuard, pollController.endPoll);

// Vote routes
app.post("/api/votes", voteController.createVote.bind(voteController)); // Create normal/point/rank vote (old format)
Expand Down
4 changes: 2 additions & 2 deletions platforms/evoting/api/src/services/DeadlineCheckService.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -80,8 +80,8 @@ export class DeadlineCheckService {
voteId: poll.id
});

// Mark that we've sent the deadline message
await this.pollRepository.update(poll.id, { deadlineMessageSent: true });
// Mark that we've sent the deadline message and set status to ended
await this.pollRepository.update(poll.id, { deadlineMessageSent: true, status: "ended" });

console.log(`Successfully sent deadline message for poll: ${poll.title} (${poll.id})`);

Expand Down
73 changes: 68 additions & 5 deletions platforms/evoting/api/src/services/PollService.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -86,8 +86,8 @@ export class PollService {
// Custom sorting based on sortField and sortDirection
const sortedPolls = filteredPolls.sort((a, b) => {
const now = new Date();
const aIsActive = !a.deadline || new Date(a.deadline) > now;
const bIsActive = !b.deadline || new Date(b.deadline) > now;
const aIsActive = a.status === "active" || (a.status !== "ended" && a.status !== "draft" && (!a.deadline || new Date(a.deadline) > now));
const bIsActive = b.status === "active" || (b.status !== "ended" && b.status !== "draft" && (!b.deadline || new Date(b.deadline) > now));

// Apply the user's chosen sorting
let comparison = 0;
Expand DownExpand Up@@ -218,13 +218,15 @@ export class PollService {
throw new Error("Blind voting (private visibility) cannot be combined with eReputation weighted voting.");
}

const hasDeadline = !!pollData.deadline;
const pollDataForEntity = {
title: pollData.title,
mode: pollData.mode as "normal" | "point" | "rank",
visibility: pollData.visibility as "public" | "private",
votingWeight: votingWeight,
options: pollData.options,
deadline: pollData.deadline ? new Date(pollData.deadline) : null,
status: hasDeadline ? "active" as const : "draft" as const,
deadline: hasDeadline ? new Date(pollData.deadline!) : null,
creator,
creatorId: pollData.creatorId,
groupId: pollData.groupId || null
Expand All@@ -237,8 +239,8 @@ export class PollService {
const savedPoll = await this.pollRepository.save(poll);
console.log('🔍 Poll saved to database:', savedPoll);

// Create a system message about the new vote
if (pollData.groupId) {
// Create a system message about the new vote (only for active polls, not drafts)
if (pollData.groupId && hasDeadline) {
await this.messageService.createVoteCreatedMessage(pollData.groupId, pollData.title, savedPoll.id, creator.name, savedPoll.deadline);
}

Expand DownExpand Up@@ -301,6 +303,67 @@ export class PollService {



/**
* Manually start a poll (draft → active). Only creator can do this.
*/
async startPoll(id: string, userId: string): Promise<Poll> {
const poll = await this.getPollById(id);
if (!poll) {
throw new Error("Poll not found");
}
if (poll.creatorId !== userId) {
throw new Error("Not authorized to start this poll");
}
if (poll.status !== "draft") {
throw new Error("Only draft polls can be started");
}

await this.pollRepository.update(id, { status: "active" });

// Send system message that voting is now open
if (poll.groupId) {
await this.messageService.createVoteCreatedMessage(
poll.groupId,
poll.title,
poll.id,
poll.creator.name,
poll.deadline
);
}

return (await this.getPollById(id))!;
}

/**
* Manually end a poll (active → ended). Only creator can do this.
*/
async endPoll(id: string, userId: string): Promise<Poll> {
const poll = await this.getPollById(id);
if (!poll) {
throw new Error("Poll not found");
}
if (poll.creatorId !== userId) {
throw new Error("Not authorized to end this poll");
}
if (poll.status !== "active") {
throw new Error("Only active polls can be ended");
}

await this.pollRepository.update(id, { status: "ended", deadlineMessageSent: true });

// Send system message that voting has ended
if (poll.groupId) {
const voteUrl = `${process.env.PUBLIC_EVOTING_URL || 'http://localhost:3000'}/${poll.id}`;
await this.messageService.createSystemMessage({
text: `eVoting Platform: Vote has been manually closed!\n\n"${poll.title}"\n\nVote ID: ${poll.id}\n\nClosed by: ${poll.creator.name}\n\n<a href="${voteUrl}" target="_blank">View results here</a>`,
groupId: poll.groupId,
voteId: poll.id
});
}

return (await this.getPollById(id))!;
}

/**
* Get polls by group ID
*/
Expand Down
24 changes: 24 additions & 0 deletions platforms/evoting/api/src/services/VoteService.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -115,6 +115,14 @@ export class VoteService {
throw new Error('Poll not found');
}

// Prevent voting on polls that are not active
if (poll.status === "draft") {
throw new Error('Voting has not started yet');
}
if (poll.status === "ended") {
throw new Error('Voting has ended');
}

// Check if user has already voted
const existingVote = await this.voteRepository.findOne({
where: { pollId, userId }
Expand DownExpand Up@@ -178,6 +186,14 @@ export class VoteService {
throw new Error('Poll not found');
}

// Prevent voting on polls that are not active
if (poll.status === "draft") {
throw new Error('Voting has not started yet');
}
if (poll.status === "ended") {
throw new Error('Voting has ended');
}

const delegation = await this.delegationRepository.findOne({
where: {
pollId,
Expand DownExpand Up@@ -660,6 +676,14 @@ export class VoteService {
throw new Error('Poll not found');
}

// Prevent voting on polls that are not active
if (poll.status === "draft") {
throw new Error('Voting has not started yet');
}
if (poll.status === "ended") {
throw new Error('Voting has ended');
}

// Create election if it doesn't exist
try {
const options = poll.options.map((opt: string, index: number) => `option_${index}`);
Expand Down
Loading