From 759b7712307687620c9e9b81209a764bd60c12c8 Mon Sep 17 00:00:00 2001 From: Merul Dhiman Date: Mon, 25 Aug 2025 03:07:44 +0530 Subject: [PATCH 1/2] fix: evoting groups --- platforms/eVoting/src/app/(app)/page.tsx | 14 ++++ .../src/controllers/PollController.ts | 5 +- .../evoting-api/src/services/PollService.ts | 61 ++++++++++++++- .../evoting-api/src/services/VoteService.ts | 75 +++++++++++++++++-- 4 files changed, 144 insertions(+), 11 deletions(-) diff --git a/platforms/eVoting/src/app/(app)/page.tsx b/platforms/eVoting/src/app/(app)/page.tsx index ca624241..f7208db9 100644 --- a/platforms/eVoting/src/app/(app)/page.tsx +++ b/platforms/eVoting/src/app/(app)/page.tsx @@ -153,6 +153,11 @@ export default function Home() { > Visibility {getSortIcon("visibility")} + + Group + handleSort("status")} @@ -196,6 +201,15 @@ export default function Home() { )} + + {poll.group ? ( + + {poll.group.name} + + ) : ( + No group + )} + {isActive ? "Active" : "Ended"} diff --git a/platforms/evoting-api/src/controllers/PollController.ts b/platforms/evoting-api/src/controllers/PollController.ts index 4c199ac3..963ef693 100644 --- a/platforms/evoting-api/src/controllers/PollController.ts +++ b/platforms/evoting-api/src/controllers/PollController.ts @@ -35,11 +35,12 @@ export class PollController { getPollById = async (req: Request, res: Response) => { try { const { id } = req.params; + const userId = (req as any).user?.id; // Get user ID from auth middleware - const poll = await this.pollService.getPollById(id); + const poll = await this.pollService.getPollByIdWithAccessCheck(id, userId); if (!poll) { - return res.status(404).json({ error: "Poll not found" }); + return res.status(404).json({ error: "Poll not found or access denied" }); } res.json(poll); diff --git a/platforms/evoting-api/src/services/PollService.ts b/platforms/evoting-api/src/services/PollService.ts index a4acfa61..eea383d2 100644 --- a/platforms/evoting-api/src/services/PollService.ts +++ b/platforms/evoting-api/src/services/PollService.ts @@ -52,10 +52,24 @@ export class PollService { } }); + // Get group information for polls that have groupId + const pollsWithGroups = await Promise.all( + allPolls.map(async (poll) => { + if (poll.groupId) { + const group = await this.groupRepository.findOne({ + where: { id: poll.groupId }, + select: ['id', 'name', 'description'] + }); + return { ...poll, group }; + } + return poll; + }) + ); + // Filter polls based on user's group memberships - let filteredPolls = allPolls; + let filteredPolls = pollsWithGroups; if (userId && userGroupIds.length > 0) { - filteredPolls = allPolls.filter(poll => { + filteredPolls = filteredPolls.filter(poll => { // Show polls that: // 1. Have no groupId (public polls) // 2. Belong to groups where user is a member/admin/participant @@ -66,7 +80,7 @@ export class PollService { }); } else if (userId) { // If user has no group memberships, only show their own polls and public polls - filteredPolls = allPolls.filter(poll => !poll.groupId || poll.creatorId === userId); + filteredPolls = filteredPolls.filter(poll => !poll.groupId || poll.creatorId === userId); } // Custom sorting based on sortField and sortDirection @@ -140,6 +154,47 @@ export class PollService { }); } + /** + * Get poll by ID with group information and check if user can access it + */ + async getPollByIdWithAccessCheck(id: string, userId?: string): Promise { + const poll = await this.pollRepository.findOne({ + where: { id }, + relations: ["creator"] + }); + + if (!poll) { + return null; + } + + // If poll has no group, it's public - anyone can access + if (!poll.groupId) { + return poll; + } + + // If no userId provided, don't show group polls + if (!userId) { + return null; + } + + // Check if user is a member, admin, or participant of the group + const group = await this.groupRepository + .createQueryBuilder('group') + .leftJoin('group.members', 'member') + .leftJoin('group.admins', 'admin') + .leftJoin('group.participants', 'participant') + .where('group.id = :groupId', { groupId: poll.groupId }) + .andWhere('(member.id = :userId OR admin.id = :userId OR participant.id = :userId)', { userId }) + .getOne(); + + // If user is not in the group, don't show the poll + if (!group) { + return null; + } + + return poll; + } + async createPoll(pollData: { title: string; mode: string; diff --git a/platforms/evoting-api/src/services/VoteService.ts b/platforms/evoting-api/src/services/VoteService.ts index ae68262d..448026b0 100644 --- a/platforms/evoting-api/src/services/VoteService.ts +++ b/platforms/evoting-api/src/services/VoteService.ts @@ -3,12 +3,14 @@ import { AppDataSource } from "../database/data-source"; import { Vote, VoteDataByMode, NormalVoteData, PointVoteData, RankVoteData } from "../database/entities/Vote"; import { Poll } from "../database/entities/Poll"; import { User } from "../database/entities/User"; +import { Group } from "../database/entities/Group"; import { VotingSystem, VoteData } from 'blindvote'; export class VoteService { private voteRepository: Repository; private pollRepository: Repository; private userRepository: Repository; + private groupRepository: Repository; // Store VotingSystem instances per poll private votingSystems = new Map(); @@ -17,11 +19,50 @@ export class VoteService { this.voteRepository = AppDataSource.getRepository(Vote); this.pollRepository = AppDataSource.getRepository(Poll); this.userRepository = AppDataSource.getRepository(User); + this.groupRepository = AppDataSource.getRepository(Group); + } + + /** + * Check if a user can vote on a poll based on group membership + */ + private async canUserVote(pollId: string, userId: string): Promise { + const poll = await this.pollRepository.findOne({ + where: { id: pollId }, + relations: ['creator'] + }); + + if (!poll) { + throw new Error('Poll not found'); + } + + // If poll has no group, it's a public poll - anyone can vote + if (!poll.groupId) { + return true; + } + + // If poll has a group, check if user is a member, admin, or participant + const group = await this.groupRepository + .createQueryBuilder('group') + .leftJoin('group.members', 'member') + .leftJoin('group.admins', 'admin') + .leftJoin('group.participants', 'participant') + .where('group.id = :groupId', { groupId: poll.groupId }) + .andWhere('(member.id = :userId OR admin.id = :userId OR participant.id = :userId)', { userId }) + .getOne(); + + if (!group) { + throw new Error('User is not a member, admin, or participant of the group associated with this poll'); + } + + return true; } // ===== NON-BLIND VOTING METHODS (for normal/point/rank modes) ===== async createVote(pollId: string, userId: string, voteData: VoteData, mode: "normal" | "point" | "rank" = "normal"): Promise { + // First check if user can vote on this poll + await this.canUserVote(pollId, userId); + const poll = await this.pollRepository.findOne({ where: { id: pollId } }); @@ -313,6 +354,15 @@ export class VoteService { async submitBlindVote(pollId: string, voterId: string, voteData: any) { try { + // First check if user can vote on this poll (group membership check) + const user = await this.userRepository.findOne({ where: { ename: voterId } }); + if (!user) { + throw new Error(`User with ename ${voterId} not found. User must exist before submitting blind vote.`); + } + + // Check group membership using the existing method + await this.canUserVote(pollId, user.id); + const votingSystem = this.getVotingSystemForPoll(pollId); // Get poll to find options @@ -374,12 +424,7 @@ export class VoteService { revealed: false }; - // For blind voting, look up the user by their ename (W3ID) - const user = await this.userRepository.findOne({ where: { ename: voterId } }); - if (!user) { - throw new Error(`User with ename ${voterId} not found. User must exist before submitting blind vote.`); - } - + // User is already fetched from the group membership check above const vote = this.voteRepository.create({ poll: { id: pollId }, user: { id: user.id }, @@ -515,6 +560,15 @@ export class VoteService { async registerBlindVoteVoter(pollId: string, voterId: string) { try { + // First check if user can vote on this poll (group membership check) + const user = await this.userRepository.findOne({ where: { ename: voterId } }); + if (!user) { + throw new Error(`User with ename ${voterId} not found. User must exist before registering for blind voting.`); + } + + // Check group membership using the existing method + await this.canUserVote(pollId, user.id); + const votingSystem = this.getVotingSystemForPoll(pollId); // Get poll details @@ -561,6 +615,15 @@ export class VoteService { // Generate vote data for a voter (used by eID wallet) async generateVoteData(pollId: string, voterId: string, chosenOptionId: string) { try { + // First check if user can vote on this poll (group membership check) + const user = await this.userRepository.findOne({ where: { ename: voterId } }); + if (!user) { + throw new Error(`User with ename ${voterId} not found. User must exist before generating vote data.`); + } + + // Check group membership using the existing method + await this.canUserVote(pollId, user.id); + const votingSystem = this.getVotingSystemForPoll(pollId); // Get poll details From 629daa5a78289fc6ba3d041309e985d6e773053e Mon Sep 17 00:00:00 2001 From: Merul Dhiman Date: Mon, 25 Aug 2025 03:29:07 +0530 Subject: [PATCH 2/2] chore: evault caching --- .../src/lib/components/EVaultList.svelte | 6 -- .../src/lib/services/cacheService.ts | 51 +++++++++++++++-- .../src/lib/services/evaultService.ts | 55 +++++++++---------- 3 files changed, 72 insertions(+), 40 deletions(-) diff --git a/infrastructure/control-panel/src/lib/components/EVaultList.svelte b/infrastructure/control-panel/src/lib/components/EVaultList.svelte index dd9c8ce3..9916d3d3 100644 --- a/infrastructure/control-panel/src/lib/components/EVaultList.svelte +++ b/infrastructure/control-panel/src/lib/components/EVaultList.svelte @@ -9,27 +9,21 @@ let cacheStatus: { lastUpdated: number; isStale: boolean; itemCount: number }; onMount(async () => { - console.log('Component mounted, starting to load eVaults...'); try { loading = true; - console.log('Loading state set to true'); await loadEVaults(); cacheStatus = EVaultService.getCacheStatus(); - console.log('Loaded eVaults:', evaults.length, 'items'); } catch (err) { error = 'Failed to load eVaults'; console.error('Error in onMount:', err); } finally { loading = false; - console.log('Loading state set to false'); } }); async function loadEVaults() { - console.log('loadEVaults called'); try { evaults = await EVaultService.getEVaults(); - console.log('EVaultService returned:', evaults.length, 'items'); cacheStatus = EVaultService.getCacheStatus(); } catch (err) { console.error('Error loading eVaults:', err); diff --git a/infrastructure/control-panel/src/lib/services/cacheService.ts b/infrastructure/control-panel/src/lib/services/cacheService.ts index 38b2bf65..9448fc9e 100644 --- a/infrastructure/control-panel/src/lib/services/cacheService.ts +++ b/infrastructure/control-panel/src/lib/services/cacheService.ts @@ -53,9 +53,19 @@ class CacheService { async getCachedEVaults(): Promise { if (typeof window !== 'undefined') { - // In browser, return null to indicate we need to fetch from server - // This prevents showing "No eVaults found" prematurely - return null as any; + // In browser, try to get from localStorage as a simple cache + try { + const cached = localStorage.getItem('evault-cache'); + if (cached) { + const data = JSON.parse(cached); + if (data.evaults && Array.isArray(data.evaults)) { + return data.evaults; + } + } + } catch (error) { + console.log('No localStorage cache available'); + } + return []; } await this.init(); @@ -77,7 +87,18 @@ class CacheService { async updateCache(evaults: EVault[]): Promise { if (typeof window !== 'undefined') { - return; // No-op in browser + // In browser, save to localStorage + try { + const cacheData = { + evaults, + lastUpdated: Date.now(), + isStale: false + }; + localStorage.setItem('evault-cache', JSON.stringify(cacheData)); + } catch (error) { + console.error('Failed to save to localStorage:', error); + } + return; } await this.init(); @@ -105,6 +126,20 @@ class CacheService { getCacheStatus(): { lastUpdated: number; isStale: boolean; itemCount: number } { if (typeof window !== 'undefined') { + // In browser, get from localStorage + try { + const cached = localStorage.getItem('evault-cache'); + if (cached) { + const data = JSON.parse(cached); + return { + lastUpdated: data.lastUpdated || 0, + isStale: data.isStale || false, + itemCount: data.evaults?.length || 0 + }; + } + } catch (error) { + console.log('No localStorage cache available'); + } return { lastUpdated: 0, isStale: true, itemCount: 0 }; } @@ -121,7 +156,13 @@ class CacheService { async clearCache(): Promise { if (typeof window !== 'undefined') { - return; // No-op in browser + // In browser, clear localStorage + try { + localStorage.removeItem('evault-cache'); + } catch (error) { + console.error('Failed to clear localStorage cache:', error); + } + return; } await this.init(); diff --git a/infrastructure/control-panel/src/lib/services/evaultService.ts b/infrastructure/control-panel/src/lib/services/evaultService.ts index c332f14c..1d17627f 100644 --- a/infrastructure/control-panel/src/lib/services/evaultService.ts +++ b/infrastructure/control-panel/src/lib/services/evaultService.ts @@ -3,34 +3,31 @@ import { cacheService } from './cacheService'; export class EVaultService { /** - * Get eVaults with stale-while-revalidate caching - * Returns cached data immediately, refreshes in background if stale + * Get eVaults - load from cache first, then fetch fresh data */ static async getEVaults(): Promise { - // In browser, always fetch from server since caching doesn't work here - if (typeof window !== 'undefined') { - return this.fetchEVaultsDirectly(); - } - - // On server, use caching - const isStale = await cacheService.isCacheStale(); - - if (isStale) { - // Cache is stale, refresh in background - this.refreshCacheInBackground(); + // First, try to get cached data (fast) + let cachedData: EVault[] = []; + try { + cachedData = await cacheService.getCachedEVaults(); + } catch (error) { + console.log('No cached data available'); } - - // Return cached data immediately (even if stale) - return await cacheService.getCachedEVaults(); + + // Fire off fresh request in background (don't wait for it) + this.fetchFreshDataInBackground(); + + // Return cached data immediately (even if empty) + return cachedData; } /** - * Force refresh the cache with fresh data + * Force refresh - get fresh data and update cache */ static async forceRefresh(): Promise { - const evaults = await this.fetchEVaultsDirectly(); - await cacheService.updateCache(evaults); - return evaults; + const freshData = await this.fetchEVaultsDirectly(); + await cacheService.updateCache(freshData); + return freshData; } /** @@ -48,21 +45,19 @@ export class EVaultService { } /** - * Refresh cache in background (non-blocking) + * Fetch fresh data in background and update cache */ - private static async refreshCacheInBackground(): Promise { + private static async fetchFreshDataInBackground(): Promise { try { - const evaults = await this.fetchEVaultsDirectly(); - await cacheService.updateCache(evaults); + const freshData = await this.fetchEVaultsDirectly(); + await cacheService.updateCache(freshData); } catch (error) { - console.error('Background cache refresh failed:', error); - // Mark cache as stale so next request will try again - await cacheService.markStale(); + console.error('Background refresh failed:', error); } } /** - * Fetch eVaults directly from Kubernetes API + * Fetch eVaults directly from API */ private static async fetchEVaultsDirectly(): Promise { try { @@ -70,7 +65,9 @@ export class EVaultService { if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } - return await response.json(); + const data = await response.json(); + // The backend returns { evaults: [...] } + return data.evaults || []; } catch (error) { console.error('Failed to fetch eVaults:', error); throw error;