diff --git a/backend/Dockerfile b/backend/Dockerfile index dc06bea..e4ffa31 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -2,6 +2,9 @@ FROM node:18-alpine WORKDIR /app + +RUN apk add --no-cache python3 make g++ bash + COPY package*.json ./ RUN npm install diff --git a/backend/constants/statusMap.js b/backend/constants/statusMap.js new file mode 100644 index 0000000..97a916e --- /dev/null +++ b/backend/constants/statusMap.js @@ -0,0 +1,12 @@ +const statusMap = { + 0: 'upcoming', + 1: 'active', + 2: 'completed', + 3: 'cancelled', +}; + +const getStatusString = status => { + return statusMap[status] || 'unknown'; +}; + +module.exports = { getStatusString }; diff --git a/backend/controllers/security/getAuditLogs.js b/backend/controllers/security/getAuditLogs.js new file mode 100644 index 0000000..f68a7ac --- /dev/null +++ b/backend/controllers/security/getAuditLogs.js @@ -0,0 +1,40 @@ +const { + fetchAllAuditLogs, + fetchAuditLogsByElectionId, +} = require('../../database/queries/security/fetchAuditLogs'); + +// Fetch all audit logs +const getAllAuditLogs = async (req, res) => { + try { + const auditLogs = await fetchAllAuditLogs(); + res.status(200).json({ auditLogs }); + } catch (error) { + res.status(500).json({ + error: 'Failed to fetch audit logs', + details: error.message, + }); + } +}; + +const getAuditLogsByElectionId = async (req, res) => { + const { electionId } = req.params; + + try { + const auditLogs = await fetchAuditLogsByElectionId(electionId); + if (!auditLogs || auditLogs.length === 0) { + return res.status(404).json({ message: 'No audit logs found for the given election ID' }); + } + + res.status(200).json({ auditLogs }); + } catch (error) { + res.status(500).json({ + error: 'Failed to fetch audit logs by election ID', + details: error.message, + }); + } +}; + +module.exports = { + getAllAuditLogs, + getAuditLogsByElectionId, +}; diff --git a/backend/controllers/security/getSecurityBreaches.js b/backend/controllers/security/getSecurityBreaches.js new file mode 100644 index 0000000..f8e2cfb --- /dev/null +++ b/backend/controllers/security/getSecurityBreaches.js @@ -0,0 +1,20 @@ +const { + fetchAllSecurityBreaches, +} = require('../../database/queries/security/fetchSecurityBreaches.js'); + +// Fetch all security breaches +const getAllSecurityBreaches = async (req, res) => { + try { + const securityBreaches = await fetchAllSecurityBreaches(); + res.status(200).json({ securityBreaches }); + } catch (error) { + res.status(500).json({ + error: 'Failed to fetch security breaches', + details: error.message, + }); + } +}; + +module.exports = { + getAllSecurityBreaches, +}; diff --git a/backend/controllers/security/modifySecurityBreach.js b/backend/controllers/security/modifySecurityBreach.js new file mode 100644 index 0000000..0ce46df --- /dev/null +++ b/backend/controllers/security/modifySecurityBreach.js @@ -0,0 +1,23 @@ +const { patchSecurityBreach } = require('../../database/queries/security/patchSecurityBreach'); + +const modifySecurityBreach = async (req, res) => { + const { breachId } = req.params; + const { resolutionStatus } = req.body; + + try { + const updatedBreach = await patchSecurityBreach(breachId, resolutionStatus); + if (!updatedBreach) { + return res.status(404).json({ message: 'Security breach not found' }); + } + + res.status(200).json({ updatedBreach }); + } catch (error) { + res.status(500).json({ + error: 'Failed to update security breach', + details: error.message, + }); + } +}; +module.exports = { + modifySecurityBreach, +}; diff --git a/backend/cron/electionSecurity.js b/backend/cron/electionSecurity.js new file mode 100644 index 0000000..780bac5 --- /dev/null +++ b/backend/cron/electionSecurity.js @@ -0,0 +1,185 @@ +const cron = require('node-cron'); +const { pool } = require('../database/db'); +const fetchAllElections = require('../database/queries/elections/fetchElection').fetchAllElections; +const { singleElectionContract } = require('../utils/thirdwebClient'); +const { runAllSecurityChecks, getCheckSummary } = require('./security-checks'); +const { updateElectionStatusesNow } = require('./electionStatus'); + +/** + * Maps AutoElection contract ElectionState enum to database status strings + * Contract enum: UPCOMING=0, ACTIVE=1, COMPLETED=2, CANCELLED=3 + */ +const mapChainStatusToDb = chainStatus => { + const statusMap = { + 0: 'upcoming', // ElectionState.UPCOMING + 1: 'active', // ElectionState.ACTIVE + 2: 'completed', // ElectionState.COMPLETED + 3: 'cancelled', // ElectionState.CANCELLED + }; + return statusMap[Number(chainStatus)] || 'unknown'; +}; + +/** + * Logging function with timestamps + */ +const logSecurityEvent = (level, electionId, message, details = null) => { + const timestamp = new Date().toISOString(); + console.log(`[${timestamp}] [${level.toUpperCase()}] Election ${electionId}: ${message}`); + if (details) { + console.log('Details:', JSON.stringify(details, null, 2)); + } +}; + +/** + * Main security check function that compares database data with smart contract data + */ +async function checkElectionSecurity() { + console.log('=== ELECTION SECURITY AUDIT STARTED ==='); + + await updateElectionStatusesNow(); + + const auditStartTime = new Date(); + let checkedCount = 0; + let totalViolations = 0; + + try { + const elections = await fetchAllElections(); + + // Filter elections with valid contract addresses + const electionsWithContracts = elections.filter( + e => + e.smart_contract_address && + e.smart_contract_address.startsWith('0x') && + e.smart_contract_address.length === 42 + ); + + console.log(`[AUDIT] Found ${electionsWithContracts.length} elections to audit`); + + for (const election of electionsWithContracts) { + try { + checkedCount++; + const contract = singleElectionContract(election.smart_contract_address); + + logSecurityEvent('info', election.id, `Running ${7} security checks...`); + + // Run all security checks + const checkResults = await runAllSecurityChecks(contract, election); + const summary = getCheckSummary(checkResults); + + // Log results + logSecurityEvent( + 'info', + election.id, + `Security audit complete: ${summary.passedChecks}/${summary.totalChecks} passed`, + summary + ); + + // Handle violations + const violations = checkResults.filter(r => !r.passed); + totalViolations += violations.length; + + if (violations.length > 0) { + // Insert breach record + const breachDescription = violations.map(v => `${v.type}: ${v.message}`).join('; '); + + await pool.query( + `INSERT INTO breaches (election_id, issue_type, description, detected_at) + VALUES ($1, $2, $3, $4)`, + [election.id, violations.map(v => v.type).join(','), breachDescription, auditStartTime] + ); + + logSecurityEvent('error', election.id, '๐Ÿšจ SECURITY VIOLATIONS DETECTED', violations); + } else { + logSecurityEvent('info', election.id, 'โœ… All security checks passed'); + } + + // Log to audit table + await pool.query( + `INSERT INTO security_audit_logs + (election_id, check_time, discrepancy_found, details) + VALUES ($1, $2, $3, $4)`, + [ + election.id, + auditStartTime, + violations.length > 0, + JSON.stringify({ + summary, + violations: violations.map(v => ({ + type: v.type, + message: v.message, + details: v.details, + })), + }), + ] + ); + } catch (electionError) { + logSecurityEvent('error', election.id, `Audit failed: ${electionError.message}`); + } + } + } catch (globalError) { + console.error('๐Ÿšจ [AUDIT] CRITICAL ERROR:', globalError.message); + } + + // Summary + console.log(`\n=== AUDIT COMPLETE ===`); + console.log(`๐Ÿ“Š Elections: ${checkedCount}`); + console.log(`โš ๏ธ Total violations: ${totalViolations}`); + console.log(`โฑ๏ธ Duration: ${Date.now() - auditStartTime.getTime()}ms`); +} + +/** + * Initialize and start the security monitoring cron job + */ +function startElectionSecurityCron() { + console.log('๐Ÿ” [CRON] Initializing Election Security Monitor...'); + + // Schedule: Run every 30 minutes + cron.schedule('*/30 * * * *', async () => { + try { + console.log('\n๐Ÿ” [CRON] Scheduled security check triggered...'); + await checkElectionSecurity(); + } catch (error) { + console.error('๐Ÿšจ [CRON] Unhandled error in scheduled security check:', error.message); + } + }); + + console.log('โœ… [CRON] Election Security Monitor started'); + console.log('๐Ÿ“… [CRON] Running every 1 minute (change to every 10 minutes in production)'); + console.log('๐Ÿ›‘ [CRON] To stop: Ctrl+C or process termination\n'); +} + +/** + * Manual trigger function for testing + */ +async function runSecurityCheckNow() { + console.log('๐Ÿ”ง [MANUAL] Triggering immediate security check...\n'); + try { + await checkElectionSecurity(); + console.log('โœ… [MANUAL] Manual security check completed successfully'); + } catch (error) { + console.error('โŒ [MANUAL] Manual security check failed:', error.message); + throw error; + } +} + +// Export functions +module.exports = { + startElectionSecurityCron, + runSecurityCheckNow, + checkElectionSecurity, + mapChainStatusToDb, +}; + +// For Testing +if (require.main === module) { + console.log('๐Ÿงช Running security check in test mode...\n'); + runSecurityCheckNow() + .then(() => { + console.log('โœ… Test completed successfully'); + process.exit(0); + }) + .catch(error => { + console.error('โŒ Test failed:', error.message); + process.exit(1); + }); +} diff --git a/backend/cron/electionStatus.js b/backend/cron/electionStatus.js new file mode 100644 index 0000000..3bf24ca --- /dev/null +++ b/backend/cron/electionStatus.js @@ -0,0 +1,171 @@ +const cron = require('node-cron'); +const { pool } = require('../database/db'); + +/** + * Determine election status based on current time vs start/end dates + */ +const determineElectionStatus = (startDate, endDate) => { + const now = new Date(); + const start = new Date(startDate); + const end = new Date(endDate); + + if (now < start) { + return 'upcoming'; + } else if (now >= start && now <= end) { + return 'active'; + } else if (now > end) { + return 'completed'; + } + return 'upcoming'; +}; + +/** + * Update election statuses based on current time + */ +async function updateElectionStatuses() { + const startTime = Date.now(); + console.log(`๐Ÿ• [CRON] Starting election status update at ${new Date().toISOString()}`); + + try { + // Get all non-cancelled, non-draft elections + const electionsResult = await pool.query(` + SELECT id, title, start_date, end_date, status, smart_contract_address + FROM elections + WHERE status != 'cancelled' + AND is_draft = false + AND revoked = false + ORDER BY id + `); + + const elections = electionsResult.rows; + console.log(`๐Ÿ“Š [CRON] Found ${elections.length} elections to check`); + + let updatedCount = 0; + const statusChanges = []; + + for (const election of elections) { + const currentStatus = election.status; + const calculatedStatus = determineElectionStatus(election.start_date, election.end_date); + + // Only update if status has changed + if (currentStatus !== calculatedStatus) { + await pool.query( + ` + UPDATE elections + SET status = $1, updated_at = CURRENT_TIMESTAMP + WHERE id = $2 + `, + [calculatedStatus, election.id] + ); + + updatedCount++; + statusChanges.push({ + id: election.id, + title: election.title, + oldStatus: currentStatus, + newStatus: calculatedStatus, + contractAddress: election.smart_contract_address, + }); + + console.log( + `โœ… [CRON] Election ${election.id} "${election.title}": ${currentStatus} โ†’ ${calculatedStatus}` + ); + } + } + + const duration = Date.now() - startTime; + + if (updatedCount > 0) { + console.log( + `[CRON] Updated ${updatedCount}/${elections.length} election statuses in ${duration}ms` + ); + console.log('๐Ÿ“‹ Status changes:', statusChanges); + } else { + console.log( + `โœจ [CRON] All ${elections.length} election statuses are up to date (${duration}ms)` + ); + } + + return { + success: true, + totalChecked: elections.length, + totalUpdated: updatedCount, + statusChanges, + duration, + }; + } catch (error) { + console.error('โŒ [CRON] Error updating election statuses:', error); + return { + success: false, + error: error.message, + duration: Date.now() - startTime, + }; + } +} + +/** + * Setup cron jobs for election status updates + */ +function setupElectionStatusCron() { + // Run every 5 minutes + const cronJob = cron.schedule( + '*/5 * * * *', + async () => { + await updateElectionStatuses(); + }, + { + scheduled: false, // Don't start automatically + timezone: 'UTC', + } + ); + + // Also run every hour as backup (in case 5-minute job fails) + const hourlyCronJob = cron.schedule( + '0 * * * *', + async () => { + console.log('๐Ÿ”„ [CRON] Running hourly election status backup check...'); + await updateElectionStatuses(); + }, + { + scheduled: false, + timezone: 'UTC', + } + ); + + console.log('โš™๏ธ Election status cron jobs configured:'); + console.log(' - Every 5 minutes: */5 * * * *'); + console.log(' - Every hour (backup): 0 * * * *'); + + return { + primary: cronJob, + backup: hourlyCronJob, + start() { + cronJob.start(); + hourlyCronJob.start(); + console.log('๐Ÿš€ Election status cron jobs started'); + + // Run once immediately to catch any pending updates + setTimeout(updateElectionStatuses, 1000); + }, + stop() { + cronJob.stop(); + hourlyCronJob.stop(); + console.log('๐Ÿ›‘ Election status cron jobs stopped'); + }, + }; +} + +/** + * Manual function to update statuses immediately (for testing) + */ +async function updateElectionStatusesNow() { + console.log('๐Ÿ”ง [MANUAL] Running manual election status update...'); + return await updateElectionStatuses(); +} + +module.exports = { + setupElectionStatusCron, + updateElectionStatusesNow, + updateElectionStatuses, + determineElectionStatus, +}; diff --git a/backend/cron/security-checks.js b/backend/cron/security-checks.js new file mode 100644 index 0000000..cd71224 --- /dev/null +++ b/backend/cron/security-checks.js @@ -0,0 +1,477 @@ +const { readContract } = require('thirdweb'); +const { pool } = require('../database/db'); +const { getStatusString } = require('../constants/statusMap'); + +/** + * Security check result structure + */ +class SecurityCheckResult { + constructor(passed, type, message, details = {}) { + this.passed = passed; + this.type = type; + this.message = message; + this.details = details; + this.timestamp = new Date().toISOString(); + } +} + +/** + * Check if vote counts match between DB and blockchain + */ +async function checkVoteCountIntegrity(contract, election) { + try { + const electionStats = await readContract({ + contract, + method: 'function getElectionStats() view returns (uint256, uint256)', + params: [], + }); + + const chainTotalVotes = Number(electionStats[0]); + const dbTotalVotes = election.total_votes || 0; + + const passed = chainTotalVotes === dbTotalVotes; + const discrepancy = Math.abs(chainTotalVotes - dbTotalVotes); + + return new SecurityCheckResult( + passed, + 'vote_count_integrity', + passed + ? 'Vote counts match' + : `Vote count mismatch: DB=${dbTotalVotes}, Chain=${chainTotalVotes}`, + { + dbVotes: dbTotalVotes, + chainVotes: chainTotalVotes, + discrepancy, + } + ); + } catch (error) { + return new SecurityCheckResult( + false, + 'vote_count_integrity', + `Failed to check vote counts: ${error.message}`, + { error: error.message } + ); + } +} + +/** + * Check if candidate counts and data match + */ +async function checkCandidateIntegrity(contract, election) { + try { + const chainCandidates = await readContract({ + contract, + method: + 'function getCandidates() view returns ((uint256 id, string name, uint256 voteCount)[])', + params: [], + }); + + const dbCandidatesResult = await pool.query( + 'SELECT id, name FROM candidates WHERE election_id = $1 ORDER BY name', + [election.id] + ); + + console.log('Chain Candidates:', dbCandidatesResult); + + const chainCandidateCount = chainCandidates.length; + const dbCandidateCount = dbCandidatesResult.rows.length; + + // Check count first + if (chainCandidateCount !== dbCandidateCount) { + return new SecurityCheckResult( + false, + 'candidate_count_mismatch', + `Candidate count mismatch: DB=${dbCandidateCount}, Chain=${chainCandidateCount}`, + { + dbCount: dbCandidateCount, + chainCount: chainCandidateCount, + chainCandidates: chainCandidates.map(c => ({ name: c.name, votes: Number(c.voteCount) })), + dbCandidates: dbCandidatesResult.rows, + } + ); + } + + // Check individual candidates + const mismatches = []; + for (const chainCandidate of chainCandidates) { + const dbCandidate = dbCandidatesResult.rows.find(db => db.name === chainCandidate.name); + + if (!dbCandidate) { + mismatches.push({ + type: 'missing_in_db', + candidateName: chainCandidate.name, + chainVotes: Number(chainCandidate.voteCount), + }); + } + } + + for (const dbCandidate of dbCandidatesResult.rows) { + const chainCandidate = chainCandidates.find(chain => chain.name === dbCandidate.name); + + if (!chainCandidate) { + mismatches.push({ + type: 'missing_in_chain', + candidateName: dbCandidate.name, + dbId: dbCandidate.id, + }); + } + } + + const passed = mismatches.length === 0; + + return new SecurityCheckResult( + passed, + 'candidate_integrity', + passed ? 'All candidates match' : `${mismatches.length} candidate mismatches found`, + { mismatches } + ); + } catch (error) { + return new SecurityCheckResult( + false, + 'candidate_integrity', + `Failed to check candidate integrity: ${error.message}`, + { error: error.message } + ); + } +} + +/** + * Check if election status matches + */ +async function checkElectionStatus(contract, election) { + try { + const chainStatus = await readContract({ + contract, + method: 'function getElectionState() view returns (uint8)', + params: [], + }); + + const chainStatusString = getStatusString(Number(chainStatus)); + const dbStatus = election.status; + + const passed = chainStatusString === dbStatus; + + return new SecurityCheckResult( + passed, + 'status_mismatch', + passed + ? 'Election status matches' + : `Status mismatch: DB="${dbStatus}", Chain="${chainStatusString}"`, + { + dbStatus, + chainStatus: chainStatusString, + chainStatusRaw: Number(chainStatus), + } + ); + } catch (error) { + return new SecurityCheckResult( + false, + 'status_check', + `Failed to check election status: ${error.message}`, + { error: error.message } + ); + } +} + +/** + * Check if election timing matches (start/end times) + */ +async function checkElectionTiming(contract, election) { + try { + const electionDetails = await readContract({ + contract, + method: + 'function getElectionDetails() view returns (uint256, string, uint256, uint256, uint8, bool, uint256)', + params: [], + }); + + const chainStartTime = Number(electionDetails[2]); + const chainEndTime = Number(electionDetails[3]); + + const dbStartTime = Math.floor(new Date(election.start_date).getTime() / 1000); + const dbEndTime = Math.floor(new Date(election.end_date).getTime() / 1000); + + const startTimeMatch = chainStartTime === dbStartTime; + const endTimeMatch = chainEndTime === dbEndTime; + const passed = startTimeMatch && endTimeMatch; + + return new SecurityCheckResult( + passed, + 'timing_mismatch', + passed ? 'Election timing matches' : `Timing mismatch detected`, + { + startTime: { + db: dbStartTime, + chain: chainStartTime, + match: startTimeMatch, + dbDate: election.start_date, + chainDate: new Date(chainStartTime * 1000).toISOString(), + }, + endTime: { + db: dbEndTime, + chain: chainEndTime, + match: endTimeMatch, + dbDate: election.end_date, + chainDate: new Date(chainEndTime * 1000).toISOString(), + }, + } + ); + } catch (error) { + return new SecurityCheckResult( + false, + 'timing_check', + `Failed to check election timing: ${error.message}`, + { error: error.message } + ); + } +} + +/** + * Check if election ownership matches + */ +async function checkElectionOwnership(contract, election) { + try { + const chainOwner = await readContract({ + contract, + method: 'function getEelectionOwner() view returns (address)', + params: [], + }); + + const dbOwner = election.owner_address; + + if (!dbOwner) { + return new SecurityCheckResult( + false, + 'ownership_check', + 'No owner address stored in database', + { chainOwner } + ); + } + + const passed = dbOwner.toLowerCase() === chainOwner.toLowerCase(); + + return new SecurityCheckResult( + passed, + 'ownership_mismatch', + passed + ? 'Election ownership matches' + : `Ownership mismatch: DB="${dbOwner}", Chain="${chainOwner}"`, + { + dbOwner, + chainOwner, + normalized: { + db: dbOwner.toLowerCase(), + chain: chainOwner.toLowerCase(), + }, + } + ); + } catch (error) { + return new SecurityCheckResult( + false, + 'ownership_check', + `Failed to check ownership: ${error.message}`, + { error: error.message } + ); + } +} + +/** + * Check for time-based status consistency + */ +async function checkTimeBasedStatus(contract, election) { + try { + const electionDetails = await readContract({ + contract, + method: + 'function getElectionDetails() view returns (uint256, string, uint256, uint256, uint8, bool, uint256)', + params: [], + }); + + const chainStartTime = Number(electionDetails[2]); + const chainEndTime = Number(electionDetails[3]); + const chainStatus = Number(electionDetails[4]); + + const currentTime = Math.floor(Date.now() / 1000); + + let expectedStatus; + if (currentTime < chainStartTime) { + expectedStatus = 0; // UPCOMING + } else if (currentTime < chainEndTime) { + expectedStatus = 1; // ACTIVE + } else { + expectedStatus = 2; // COMPLETED + } + + // Don't flag if manually cancelled + const passed = chainStatus === expectedStatus || chainStatus === 3; // 3 = CANCELLED + + return new SecurityCheckResult( + passed, + 'time_status_inconsistency', + passed + ? 'Status consistent with time' + : `Status should be ${expectedStatus} but is ${chainStatus}`, + { + currentTime, + chainStartTime, + chainEndTime, + chainStatus, + expectedStatus, + timestamps: { + current: new Date(currentTime * 1000).toISOString(), + start: new Date(chainStartTime * 1000).toISOString(), + end: new Date(chainEndTime * 1000).toISOString(), + }, + } + ); + } catch (error) { + return new SecurityCheckResult( + false, + 'time_status_check', + `Failed to check time-based status: ${error.message}`, + { error: error.message } + ); + } +} + +/** + * Check individual candidate vote counts + */ +async function checkIndividualCandidateVotes(contract, election) { + try { + const chainCandidates = await readContract({ + contract, + method: + 'function getCandidates() view returns ((uint256 id, string name, uint256 voteCount)[])', + params: [], + }); + + const voteDiscrepancies = []; + + for (const chainCandidate of chainCandidates) { + // In your DB, you don't seem to store individual candidate vote counts + // This is actually a security gap! You should add a vote_count column to candidates table + + // For now, we'll check if candidate exists and log the chain vote count + console.log(chainCandidate); + const dbCandidate = await pool.query( + 'SELECT id, name FROM candidates WHERE election_id = $1 AND name = $2', + [election.id, chainCandidate.name] + ); + + if (dbCandidate.rows.length === 0) { + voteDiscrepancies.push({ + candidateName: chainCandidate.name, + issue: 'candidate_missing_in_db', + chainVotes: Number(chainCandidate.voteCount), + }); + } else { + // TODO: Once you add vote_count column to candidates table, + // MIGRATION: ALTER TABLE candidates ADD COLUMN vote_count INTEGER DEFAULT 0; + // uncomment this to check individual vote counts + /* + const dbVoteCount = dbCandidate.rows[0].vote_count || 0; + const chainVoteCount = Number(chainCandidate.voteCount); + + if (dbVoteCount !== chainVoteCount) { + voteDiscrepancies.push({ + candidateName: chainCandidate.name, + issue: 'vote_count_mismatch', + dbVotes: dbVoteCount, + chainVotes: chainVoteCount, + discrepancy: Math.abs(dbVoteCount - chainVoteCount) + }); + } + */ + } + } + + const passed = voteDiscrepancies.length === 0; + + return new SecurityCheckResult( + passed, + 'individual_candidate_votes', + passed + ? 'Individual candidate votes consistent' + : `${voteDiscrepancies.length} candidate vote discrepancies`, + { + discrepancies: voteDiscrepancies, + note: 'Add vote_count column to candidates table for full vote integrity checking', + } + ); + } catch (error) { + return new SecurityCheckResult( + false, + 'individual_candidate_votes', + `Failed to check individual candidate votes: ${error.message}`, + { error: error.message } + ); + } +} + +/** + * Run all security checks for an election + */ +async function runAllSecurityChecks(contract, election) { + const checks = [ + checkVoteCountIntegrity, + checkCandidateIntegrity, + checkElectionStatus, + checkElectionTiming, + checkElectionOwnership, + checkTimeBasedStatus, + checkIndividualCandidateVotes, + ]; + + const results = []; + + for (const checkFunction of checks) { + try { + const result = await checkFunction(contract, election); + results.push(result); + } catch (error) { + results.push( + new SecurityCheckResult( + false, + 'check_execution_error', + `Failed to execute ${checkFunction.name}: ${error.message}`, + { checkFunction: checkFunction.name, error: error.message } + ) + ); + } + } + + return results; +} + +/** + * Get summary of security check results + */ +function getCheckSummary(results) { + const passed = results.filter(r => r.passed); + const failed = results.filter(r => !r.passed); + + return { + totalChecks: results.length, + passedChecks: passed.length, + failedChecks: failed.length, + passRate: (passed.length / results.length) * 100, + failedTypes: failed.map(f => f.type), + criticalIssues: failed.filter(f => + ['vote_count_integrity', 'ownership_mismatch', 'timing_mismatch'].includes(f.type) + ).length, + }; +} + +module.exports = { + SecurityCheckResult, + checkVoteCountIntegrity, + checkCandidateIntegrity, + checkElectionStatus, + checkElectionTiming, + checkElectionOwnership, + checkTimeBasedStatus, + checkIndividualCandidateVotes, + runAllSecurityChecks, + getCheckSummary, +}; diff --git a/backend/database/migrations/createTables.js b/backend/database/migrations/createTables.js index ae219c9..2f6443e 100644 --- a/backend/database/migrations/createTables.js +++ b/backend/database/migrations/createTables.js @@ -73,7 +73,6 @@ async function createTables() { ); `); - await client.query(` CREATE TABLE IF NOT EXISTS user_created_elections ( user_id INTEGER REFERENCES users(id) ON DELETE CASCADE, @@ -114,6 +113,32 @@ async function createTables() { ); `); + await client.query(` + CREATE TABLE IF NOT EXISTS security_audit_logs ( + id SERIAL PRIMARY KEY, + election_id INTEGER REFERENCES elections(id) ON DELETE CASCADE, + check_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + db_total_votes INT, + chain_total_votes INT, + db_status TEXT, + chain_status TEXT, + discrepancy_found BOOLEAN DEFAULT FALSE, + details TEXT -- e.g. JSON payload describing mismatches + ); + `); + + await client.query(` + CREATE TABLE IF NOT EXISTS breaches ( + id SERIAL PRIMARY KEY, + election_id INTEGER REFERENCES elections(id) ON DELETE CASCADE, + detected_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + issue_type TEXT, -- e.g. "vote_mismatch", "status_mismatch", "tampering" + description TEXT, + resolved BOOLEAN DEFAULT FALSE, + resolved_at TIMESTAMP + ); + `); + await client.query('COMMIT'); console.log('All tables created successfully.'); } catch (error) { @@ -128,5 +153,5 @@ async function createTables() { // createTables() module.exports = { - createTables -}; \ No newline at end of file + createTables, +}; diff --git a/backend/database/queries/security/fetchAuditLogs.js b/backend/database/queries/security/fetchAuditLogs.js new file mode 100644 index 0000000..ba6ed4c --- /dev/null +++ b/backend/database/queries/security/fetchAuditLogs.js @@ -0,0 +1,32 @@ +const { pool } = require('../../db'); +const { DatabaseError } = require('../../utils/errors'); + +async function fetchAllAuditLogs() { + try { + const result = await pool.query( + `SELECT * FROM security_audit_logs ORDER BY check_time DESC LIMIT 100` + ); + return result.rows; + } catch (error) { + throw new DatabaseError('Error fetching audit logs: ' + error.message); + } +} + +async function fetchAuditLogsByElectionId(electionId) { + try { + const result = await pool.query( + `SELECT * FROM security_audit_logs + WHERE election_id = $1 + ORDER BY check_time DESC`, + [electionId] + ); + return result.rows[0]; + } catch (error) { + throw new DatabaseError('Error fetching election: ' + error.message); + } +} + +module.exports = { + fetchAllAuditLogs, + fetchAuditLogsByElectionId, +}; diff --git a/backend/database/queries/security/fetchSecurityBreaches.js b/backend/database/queries/security/fetchSecurityBreaches.js new file mode 100644 index 0000000..3db13a0 --- /dev/null +++ b/backend/database/queries/security/fetchSecurityBreaches.js @@ -0,0 +1,25 @@ +const { pool } = require('../../db'); +const { DatabaseError } = require('../../utils/errors'); + +async function fetchAllSecurityBreaches() { + try { + const result = await pool.query(`SELECT * FROM breaches ORDER BY detected_at DESC LIMIT 100`); + return result.rows; + } catch (error) { + throw new DatabaseError('Error fetching security breaches: ' + error.message); + } +} + +async function fetchSecurityBreachById(breachId) { + try { + const result = await pool.query(`SELECT * FROM breaches WHERE id = $1`, [breachId]); + return result.rows[0]; + } catch (error) { + throw new DatabaseError('Error fetching security breach: ' + error.message); + } +} + +module.exports = { + fetchAllSecurityBreaches, + fetchSecurityBreachById, +}; diff --git a/backend/database/queries/security/patchSecurityBreach.js b/backend/database/queries/security/patchSecurityBreach.js new file mode 100644 index 0000000..7c2ad57 --- /dev/null +++ b/backend/database/queries/security/patchSecurityBreach.js @@ -0,0 +1,18 @@ +const { pool } = require('../../db'); +const { DatabaseError } = require('../../utils/errors'); + +async function patchSecurityBreach(breachId, resolutionStatus) { + try { + await pool.query(`UPDATE breaches SET resolution_status = $1 WHERE id = $2 RETURNING *`, [ + resolutionStatus, + breachId, + ]); + res.json({ success: true }); + } catch (error) { + throw new DatabaseError('Error modifying security breach: ' + error.message); + } +} + +module.exports = { + patchSecurityBreach, +}; diff --git a/backend/index.js b/backend/index.js index c4febac..68d5177 100644 --- a/backend/index.js +++ b/backend/index.js @@ -10,13 +10,16 @@ let { RedisStore } = require('connect-redis'); const userRoutes = require('./routes/userRoutes.js'); const electionRoutes = require('./routes/electionRoutes.js'); const kycRoutes = require('./routes/kyc.js'); +const securityRoutes = require('./routes/securityRoutes.js'); const { createTables } = require('./database/migrations/createTables.js'); +const { startElectionSecurityCron } = require('./cron/electionSecurity.js'); +const setupElectionStatusCron = require('./cron/electionStatus.js'); const app = express(); const server = createServer(app); const port = 3001; -createTables() +createTables(); app.use(express.json()); @@ -75,11 +78,16 @@ app.use((req, res, next) => { app.use('/api/user', userRoutes); app.use('/api/admin', electionRoutes); app.use('/api/kyc', kycRoutes); +app.use('/api/security', securityRoutes); app.get('/', (req, res) => { res.status(200).json({ message: 'Welcome to the voting system' }); }); +// CRON JOBS +// setupElectionStatusCron(); +startElectionSecurityCron(); + server.listen(port, '0.0.0.0', () => { console.log(`Backend + WebSocket server running at http://localhost:${port}`); }); diff --git a/backend/package.json b/backend/package.json index eeb34c1..3a36fff 100644 --- a/backend/package.json +++ b/backend/package.json @@ -10,19 +10,23 @@ "license": "ISC", "description": "", "devDependencies": { + "@types/node": "^24.3.3", "cors": "^2.8.5", - "ethers": "^6.13.5", "express": "^4.21.2", - "nodemon": "^3.1.9" + "nodemon": "^3.1.9", + "typescript": "^5.9.2" }, "dependencies": { + "@thirdweb-dev/sdk": "^4.0.99", "axios": "^1.9.0", "bcrypt": "^5.1.1", "connect-redis": "^8.0.1", "didit-node-client": "^1.0.4", "dotenv": "^16.4.7", + "ethers": "^5.8.0", "express-session": "^1.18.1", "jsonwebtoken": "^9.0.2", + "node-cron": "^4.2.1", "nodemailer": "^7.0.6", "pg": "^8.13.3", "redis": "^4.7.0", diff --git a/backend/routes/securityRoutes.js b/backend/routes/securityRoutes.js new file mode 100644 index 0000000..e94cf9a --- /dev/null +++ b/backend/routes/securityRoutes.js @@ -0,0 +1,17 @@ +const express = require('express'); +const router = express.Router(); +const authenticateSession = require('../middleware/authenticateSession.js'); +const { + getAllAuditLogs, + getAuditLogsByElectionId, +} = require('../controllers/security/getAuditLogs.js'); +const { getAllSecurityBreaches } = require('../controllers/security/getSecurityBreaches.js'); +const { modifySecurityBreach } = require('../controllers/security/modifySecurityBreach.js'); + +router.get('/audit-logs', authenticateSession, getAllAuditLogs); +router.get('/audit-logs/:electionId', authenticateSession, getAuditLogsByElectionId); + +router.get('/breaches', authenticateSession, getAllSecurityBreaches); +router.patch('/breaches/:breachId', authenticateSession, modifySecurityBreach); + +module.exports = router; diff --git a/backend/utils/thirdwebClient.js b/backend/utils/thirdwebClient.js new file mode 100644 index 0000000..b23061d --- /dev/null +++ b/backend/utils/thirdwebClient.js @@ -0,0 +1,34 @@ +const { createThirdwebClient, getContract } = require('thirdweb'); +const { sepolia } = require('thirdweb/chains'); + +// Use Node.js environment variables +// const clientId = process.env.TEMPLATE_CLIENT_ID; +const clientId = '7587edc8de012685eb8c9083058548e6'; +const contractFactoryAddress = '0xB2C5664f8FA88523DEb5761dA77fCea6B507303f'; + +// Create a Thirdweb client +const client = createThirdwebClient({ + clientId, +}); + +// Preload the election factory contract +const electionFactoryContract = getContract({ + client, + chain: sepolia, + address: contractFactoryAddress, +}); + +// Utility to get a single election contract by address +const singleElectionContract = address => { + return getContract({ + client, + chain: sepolia, + address, + }); +}; + +module.exports = { + client, + electionFactoryContract, + singleElectionContract, +}; diff --git a/docker-compose.yaml b/docker-compose.yaml index 6a094a0..e814d0e 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,4 +1,4 @@ -version: "3.8" +version: '3.8' services: postgres: image: postgres:15 @@ -9,9 +9,9 @@ services: POSTGRES_PASSWORD: postgres POSTGRES_DB: newdb ports: - - "5432:5432" + - '5432:5432' volumes: - - postgres_data:/var/lib/postgresql/data + - postgres_data:/var/lib/postgresql/data backend: build: @@ -21,10 +21,10 @@ services: restart: always depends_on: - postgres - environment: - DATABASE_URL: postgres://postgres:postgres@postgres:5432/newdb + env_file: + - ./backend/.env ports: - - "3001:3001" + - '3001:3001' volumes: postgres_data: diff --git a/frontend/.env.example b/frontend/.env.example index 8978e8a..6000e68 100644 --- a/frontend/.env.example +++ b/frontend/.env.example @@ -1,7 +1,7 @@ VITE_TEMPLATE_CLIENT_ID=753b0e6f63a4969e794defddff79537c VITE_THIRDWEB_API_KEY=6vMvVqAP24AXACxjC11PPHJio8itw43d VITE_THIRDWEB_CLIENT_ID=7587edc8de012685eb8c9083058548e6 -VITE_THIRDWEB_FACTORY_ADDRESS=0xF100E06BEdFD52cdA442e310859F232D87407299 +VITE_THIRDWEB_FACTORY_ADDRESS=0xB2C5664f8FA88523DEb5761dA77fCea6B507303f CLOUDINARY_URL=cloudinary://515658968744785:BlyrdTTicihZ_yeBRL_4h5-nid4@dki1hiyny SECRET_KEY=1gkAyRwC-CBtkTcXTiA2imx3yzb_n_YxHFrf7btj8AkrRCPWzK59VFssmpSWjsjiLaHP50byIlFVFroB_6JATw VITE_ETHERSCAN_KEY=14V8HFAEK4W6353RP5B6DGT161F9EBHQTJ diff --git a/frontend/package.json b/frontend/package.json index 11259a7..76e3dde 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -51,6 +51,9 @@ "didit-sdk": "^2.0.14", "embla-carousel-react": "^8.3.0", "framer-motion": "^12.9.2", + "i18next": "^25.5.2", + "i18next-browser-languagedetector": "^8.2.0", + "i18next-http-backend": "^3.0.2", "input-otp": "^1.2.4", "install": "^0.13.0", "lucide-react": "^0.462.0", @@ -59,6 +62,7 @@ "react-day-picker": "^8.10.1", "react-dom": "^18.3.1", "react-hook-form": "^7.53.0", + "react-i18next": "^15.7.3", "react-resizable-panels": "^2.1.3", "react-router-dom": "^6.26.2", "react-router-form": "^3.0.0", diff --git a/frontend/src/api/config.ts b/frontend/src/api/config.ts index 4783177..001d397 100644 --- a/frontend/src/api/config.ts +++ b/frontend/src/api/config.ts @@ -5,13 +5,14 @@ const isIpAccess = window.location.hostname !== 'localhost'; // Use the same hostname the browser is using, but port 3001 for backend const API_BASE_URL = `http://${window.location.hostname}:3001/api`; +// const DEPLOYED_API_BASE_URL = 'http://13.61.180.232:3001/api'; const apiClient = axios.create({ baseURL: API_BASE_URL, headers: { 'Content-Type': 'application/json', }, - withCredentials: true, + withCredentials: true, }); -export default apiClient; \ No newline at end of file +export default apiClient; diff --git a/frontend/src/api/index.ts b/frontend/src/api/index.ts index 0ce18d1..f34cd07 100644 --- a/frontend/src/api/index.ts +++ b/frontend/src/api/index.ts @@ -1,13 +1,7 @@ - import apiClient from './config'; import authService from './services/authService'; import userService from './services/userService'; import electionService from './services/electionService'; +import securityService from './services/securityService'; - -export { - apiClient, - authService, - userService, - electionService -}; +export { apiClient, authService, securityService, userService, electionService }; diff --git a/frontend/src/api/services/electionService.ts b/frontend/src/api/services/electionService.ts index f89cb05..e845db6 100644 --- a/frontend/src/api/services/electionService.ts +++ b/frontend/src/api/services/electionService.ts @@ -1,16 +1,15 @@ - import CandidateCard from '@/components/election/CandidateCard'; import apiClient from '../config'; import { Vote } from 'lucide-react'; export interface ElectionRequest { - id?: number, + id?: number; title: string; description: string; rules: string[]; startDate: Date; endDate: Date; - status?: 'upcoming' | 'active' | 'completed' | 'cancelled'; + status?: 'upcoming' | 'active' | 'completed' | 'cancelled' | 'draft'; imageURL?: string; organization?: string; isPublic: boolean; @@ -31,7 +30,6 @@ export interface CandidateRequest { } const electionService = { - createElection: async (electionData: ElectionRequest) => { try { //TODO: fix the date format here please @@ -40,7 +38,6 @@ const electionService = { const response = await apiClient.post('/admin/election', electionData); return response.data; } catch (error) { - if (error.response) { throw new Error(error.response.data.message || 'Failed to create election'); } else if (error.request) { @@ -75,7 +72,6 @@ const electionService = { return response.data; }, - getElection: async (id: number) => { const response = await apiClient.get(`/admin/election/${id}`); return response.data; @@ -84,7 +80,7 @@ const electionService = { vote: async (electionId: number) => { const response = await apiClient.put(`/admin/election/vote/${electionId}`); return response.data; - } + }, // updateElection: async (id: number, updates: Partial) => { // const response = await apiClient.patch(`/admin/election/${id}`, updates); @@ -92,4 +88,4 @@ const electionService = { // }, }; -export default electionService; \ No newline at end of file +export default electionService; diff --git a/frontend/src/api/services/securityService.ts b/frontend/src/api/services/securityService.ts new file mode 100644 index 0000000..e275c35 --- /dev/null +++ b/frontend/src/api/services/securityService.ts @@ -0,0 +1,47 @@ +import apiClient from '../config'; + +export interface SecurityLog { + id: number; + election_id: number; + type: string; + message: string; + severity: 'low' | 'medium' | 'high'; + created_at: string; + resolved?: boolean; +} + +const securityService = { + // Fetch all security logs + getAuditLogs: async () => { + try { + const response = await apiClient.get('/security/audit-logs'); + return response.data.auditLogs ?? []; + } catch (error) { + console.error('Failed to fetch audit logs', error); + throw error; + } + }, + // Fetch all security breaches + getBreaches: async (): Promise => { + try { + const response = await apiClient.get('/security/breaches'); + return response.data.securityBreaches ?? []; + } catch (error) { + console.error('Failed to fetch security breaches', error); + throw error; + } + }, + + // Mark a breach/log as resolved + resolveBreach: async (id: number, resolutionStatus = true): Promise => { + try { + const response = await apiClient.patch(`/security/breaches/${id}`, { resolutionStatus }); + return response.data; + } catch (error) { + console.error('Failed to resolve security breach', error); + throw error; + } + }, +}; + +export default securityService; diff --git a/frontend/src/auth/AdminRoute.tsx b/frontend/src/auth/AdminRoute.tsx index 1b40eac..8a1639c 100644 --- a/frontend/src/auth/AdminRoute.tsx +++ b/frontend/src/auth/AdminRoute.tsx @@ -2,7 +2,7 @@ import { Navigate } from 'react-router-dom'; import { useAuth } from './AuthProvider'; const AdminRoute = ({ children }: { children: JSX.Element }) => { - const { user } = useAuth(); + const { user, isLoading } = useAuth(); if (!user) { return ; @@ -15,4 +15,4 @@ const AdminRoute = ({ children }: { children: JSX.Element }) => { return children; }; -export { AdminRoute}; \ No newline at end of file +export { AdminRoute }; diff --git a/frontend/src/components/Cta.tsx b/frontend/src/components/Cta.tsx index 3054389..ff40830 100644 --- a/frontend/src/components/Cta.tsx +++ b/frontend/src/components/Cta.tsx @@ -3,9 +3,11 @@ import { motion } from 'framer-motion'; import { Button } from './ui/button'; import { ArrowRight, PlayCircle } from 'lucide-react'; import { useNavigate } from 'react-router-dom'; +import { useTranslation } from 'react-i18next'; const Cta = () => { const navigate = useNavigate(); + const { t } = useTranslation(); const containerVariants = { hidden: { opacity: 0 }, @@ -13,9 +15,9 @@ const Cta = () => { opacity: 1, transition: { staggerChildren: 0.2, - delayChildren: 0.3 - } - } + delayChildren: 0.3, + }, + }, }; const itemVariants = { @@ -25,27 +27,27 @@ const Cta = () => { y: 0, transition: { duration: 0.5, - ease: "easeOut" - } - } + ease: 'easeOut', + }, + }, }; const onContact = () => { - navigate("/contact"); - } + navigate('/contact'); + }; return (
{/* Background gradient overlay */}
- + {/* Animated background pattern */}
- {
- +
- Get Started + {t('cta.badge')} - - Ready to Transform Voting For Your Organization? + {t('cta.title')} - - Whether you're running a national election, corporate voting, or university elections, - SmartVote provides the security and transparency you need. + + {t('cta.subtitle')} - - - + {t('cta.buttons.watchDemo')} + - - + - - No credit card required โ€ข free trial โ€ข anytime โ€ข anywhere + + {t('cta.disclaimer')}
diff --git a/frontend/src/components/Faq.tsx b/frontend/src/components/Faq.tsx index 8562664..1f83ed0 100644 --- a/frontend/src/components/Faq.tsx +++ b/frontend/src/components/Faq.tsx @@ -1,51 +1,42 @@ import React from 'react'; import { motion } from 'framer-motion'; import { ChevronDown, HelpCircle } from 'lucide-react'; -import { - Accordion, - AccordionContent, - AccordionItem, - AccordionTrigger, -} from './ui/accordion'; +import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from './ui/accordion'; import { Button } from './ui/button'; import { ArrowRight } from 'lucide-react'; import { useNavigate } from 'react-router-dom'; +import { useTranslation } from 'react-i18next'; const faqData = [ { - question: "What is blockchain voting?", - answer: "Blockchain voting uses distributed ledger technology to create a secure, transparent, and immutable record of votes. Each vote is encrypted and added to a Ethereum blockchain, making it virtually impossible to alter or delete votes once they've been cast.", - color: "from-blue-500 to-cyan-500" + key: 'blockchain', + color: 'from-blue-500 to-cyan-500', }, { - question: "Is blockchain voting secure?", - answer: "Yes, blockchain voting is extremely secure. It uses cryptographic techniques to protect votes, distributes the voting record across multiple nodes to prevent tampering, and provides end-to-end encryption for voter privacy. Our system also undergoes regular security audits and penetration testing.", - color: "from-vote-blue to-vote-teal" + key: 'security', + color: 'from-vote-blue to-vote-teal', }, { - question: "How does SmartVote ensure voter privacy?", - answer: "SmartVote uses advanced cryptographic techniques including zero-knowledge proofs that allow voters to verify their vote was counted correctly without revealing who they voted for. The system separates voter identity verification from the actual voting process to ensure complete anonymity.", - color: "from-indigo-500 to-vote-blue" + key: 'privacy', + color: 'from-indigo-500 to-vote-blue', }, { - question: "Can voters verify their votes were counted correctly?", - answer: "Yes, each voter receives a unique, anonymous receipt with a cryptographic key that allows them to verify their vote was recorded correctly on the blockchain without revealing their identity or how they voted.", - color: "from-vote-teal to-emerald-500" + key: 'verification', + color: 'from-vote-teal to-emerald-500', }, { - question: "What happens if there's a technical issue during voting?", - answer: "Our system has multiple redundancies built in. If a voter experiences a technical issue, they can restart the voting process, and the system ensures no duplicate votes are counted. We also provide 24/7 technical support during election periods.", - color: "from-vote-blue to-cyan-500" + key: 'technical', + color: 'from-vote-blue to-cyan-500', }, { - question: "Is SmartVote accessible to all voters?", - answer: "Yes, SmartVote is designed with accessibility as a priority. The platform complies with WCAG 2.1 AA standards, supports screen readers, offers keyboard navigation, and provides interfaces in multiple languages. For voters without internet access, we work with election authorities to provide voting kiosks at traditional polling locations.", - color: "from-vote-teal to-emerald-500" - } + key: 'accessibility', + color: 'from-vote-teal to-emerald-500', + }, ]; const Faq = () => { const navigate = useNavigate(); + const { t } = useTranslation(); const containerVariants = { hidden: { opacity: 0 }, @@ -53,9 +44,9 @@ const Faq = () => { opacity: 1, transition: { staggerChildren: 0.1, - delayChildren: 0.3 - } - } + delayChildren: 0.3, + }, + }, }; const itemVariants = { @@ -65,19 +56,19 @@ const Faq = () => { y: 0, transition: { duration: 0.5, - ease: "easeOut" - } - } + ease: 'easeOut', + }, + }, }; const onStillHaveQuestions = () => { - navigate("/contact") - } + navigate('/contact'); + }; return (
- { > - FAQ + {t('faq.badge')} - - Frequently Asked Questions + {t('faq.title')} - - Everything you need to know about our blockchain voting system + + {t('faq.subtitle')} - { > {faqData.map((faq, index) => ( - - + +
-
+
- {faq.question} + {t(`faq.questions.${faq.key}.question`)}
- {faq.answer} + {t(`faq.questions.${faq.key}.answer`)} ))} - - diff --git a/frontend/src/components/Features.tsx b/frontend/src/components/Features.tsx index dd24d38..e4be141 100644 --- a/frontend/src/components/Features.tsx +++ b/frontend/src/components/Features.tsx @@ -3,46 +3,44 @@ import { Shield, Users, Lock, Calendar, ArrowRight } from 'lucide-react'; import { motion } from 'framer-motion'; import { Button } from './ui/button'; import { useNavigate } from 'react-router-dom'; +import { useTranslation } from 'react-i18next'; const featureData = [ { icon: Shield, - title: 'Tamper-Proof Security', - description: "Leverage blockchain's immutability to create a voting system that is virtually impossible to hack or manipulate.", - color: 'from-blue-500 to-cyan-500' + key: 'tamperProof', + color: 'from-blue-500 to-cyan-500', }, { icon: Users, - title: 'Full Transparency', - description: 'All votes are verifiable on the public blockchain, allowing for transparency while maintaining voter privacy.', - color: 'from-vote-blue to-vote-teal' + key: 'transparency', + color: 'from-vote-blue to-vote-teal', }, { icon: Lock, - title: 'End-to-End Encryption', - description: 'Advanced cryptographic techniques ensure your vote remains secure and private from submission to counting.', - color: 'from-indigo-500 to-vote-blue' + key: 'encryption', + color: 'from-indigo-500 to-vote-blue', }, { icon: Calendar, - title: 'Instant Results', - description: 'Eliminate waiting periods with smart contracts vote counting, providing immediate and accurate election results.', - color: 'from-vote-teal to-emerald-500' - } + key: 'instantResults', + color: 'from-vote-teal to-emerald-500', + }, ]; const Features = () => { - const navigate = useNavigate(); + const { t } = useTranslation(); + const containerVariants = { hidden: { opacity: 0 }, visible: { opacity: 1, transition: { staggerChildren: 0.2, - delayChildren: 0.3 - } - } + delayChildren: 0.3, + }, + }, }; const itemVariants = { @@ -52,56 +50,53 @@ const Features = () => { y: 0, transition: { duration: 0.5, - ease: "easeOut" - } - } + ease: 'easeOut', + }, + }, }; const onLearnMore = () => { - navigate("/Dashboard"); - } + navigate('/Dashboard'); + }; return (
- - Features + {t('features.badge')} - - Features for Blockchain Voting + {t('features.title')} - - Our blockchain voting system combines cutting-edge technology with user-friendly design to redefine electoral processes. + + {t('features.subtitle')} - {featureData.map((feature, index) => ( {
-

{feature.title}

-

{feature.description}

- -
+
))} - - diff --git a/frontend/src/components/Footer.tsx b/frontend/src/components/Footer.tsx index 8c817a8..ea1cff0 100644 --- a/frontend/src/components/Footer.tsx +++ b/frontend/src/components/Footer.tsx @@ -1,98 +1,159 @@ import React from 'react'; import { Link } from 'react-router-dom'; import { motion } from 'framer-motion'; -import { ArrowRight } from 'lucide-react'; +import { Facebook, Twitter, Linkedin, Instagram, Mail, Phone, MapPin } from 'lucide-react'; +import { useTranslation } from 'react-i18next'; const Footer = () => { - const currentYear = new Date().getFullYear(); - - const footerLinks = [ - { name: "Contact", href: "/contact" }, - { name: "Privacy", href: "/privacy" } + const { t } = useTranslation(); + + const quickLinks = [ + { key: 'home', href: '/' }, + { key: 'features', href: '#features' }, + { key: 'howItWorks', href: '#how-it-works' }, + { key: 'security', href: '#security' }, + ]; + + const companyLinks = [ + { key: 'about', href: '/about' }, + { key: 'careers', href: '/careers' }, + { key: 'blog', href: '/blog' }, + { key: 'press', href: '/press' }, + ]; + + const supportLinks = [ + { key: 'help', href: '/help' }, + { key: 'contact', href: '/contact' }, + ]; + + const legalLinks = [ + { key: 'privacy', href: '/privacy' }, + { key: 'terms', href: '/terms' }, + ]; + + const socialLinks = [ + { icon: Facebook, href: 'https://facebook.com', label: 'Facebook' }, + { icon: Twitter, href: 'https://twitter.com', label: 'Twitter' }, + { icon: Linkedin, href: 'https://linkedin.com', label: 'LinkedIn' }, + { icon: Instagram, href: 'https://instagram.com', label: 'Instagram' }, ]; - const containerVariants = { - hidden: { opacity: 0, y: 20 }, - visible: { - opacity: 1, - y: 0, - transition: { - duration: 0.5, - ease: "easeOut" - } - } - }; - return ( - -
-
- {/* Logo */} - +
+
+ {/* Brand Section */} + - SmartVote + + + SmartVote + +

{t('footer.description')}

+
+ {socialLinks.map((social, index) => ( + + + + ))} +
+
- {/* Navigation Links */} - + {/* Quick Links */} + +

{t('footer.quickLinks')}

+
    + {quickLinks.map((link) => ( +
  • + + {t(`footer.${link.key}`)} + +
  • + ))} +
+
- {/* Social Links */} - + {/* Company */} + +

{t('footer.company')}

+
    + {companyLinks.map((link) => ( +
  • + + {t(`footer.${link.key}`)} + +
  • + ))} +
+
- {/* Copyright */} -

- ยฉ {currentYear} SmartVote. All rights reserved. -

+ {/* Support & Legal */} + +

{t('footer.support')}

+
    + {supportLinks.map((link) => ( +
  • + + {t(`footer.${link.key}`)} + +
  • + ))} +
+ +

{t('footer.legal')}

+
    + {legalLinks.map((link) => ( +
  • + + {t(`footer.${link.key}`)} + +
  • + ))} +
+
+ + {/* Bottom Section */} + +

{t('footer.copyright')}

+
- + ); }; diff --git a/frontend/src/components/Hero.tsx b/frontend/src/components/Hero.tsx index 62c8181..a01e6a0 100644 --- a/frontend/src/components/Hero.tsx +++ b/frontend/src/components/Hero.tsx @@ -3,9 +3,11 @@ import { Button } from './ui/button'; import { Shield, Check, Lock, ArrowRight } from 'lucide-react'; import { motion } from 'framer-motion'; import { useNavigate } from 'react-router-dom'; +import { useTranslation } from 'react-i18next'; const Hero = () => { const navigate = useNavigate(); + const { t } = useTranslation(); const containerVariants = { hidden: { opacity: 0 }, @@ -13,9 +15,9 @@ const Hero = () => { opacity: 1, transition: { staggerChildren: 0.2, - delayChildren: 0.3 - } - } + delayChildren: 0.3, + }, + }, }; const itemVariants = { @@ -25,23 +27,23 @@ const Hero = () => { y: 0, transition: { duration: 0.5, - ease: "easeOut" - } - } + ease: 'easeOut', + }, + }, }; return (
{/* Background gradient overlay */}
- + {/* Animated background pattern */}
- { > - Your Vote Counts! + {t('hero.badge')} - - Voting using Blockchain Technology + {t('hero.title')} - - Transform electoral systems with decentralized security and transparency. - Our blockchain technology ensures tamper-proof voting that's accessible to everyone. + + {t('hero.subtitle')} - - - + - - + {t('hero.buttons.viewDemo')} + - + {[ - { icon: Shield, text: "Tamper-proof", delay: 0.1 }, - { icon: Check, text: "Transparent", delay: 0.2 }, - { icon: Lock, text: "End-to-end encrypted", delay: 0.3 } + { icon: Shield, text: t('hero.features.tamperProof'), delay: 0.1 }, + { icon: Check, text: t('hero.features.transparent'), delay: 0.2 }, + { icon: Lock, text: t('hero.features.encrypted'), delay: 0.3 }, ].map((item, index) => ( + >
-
+
{item.text} ))} @@ -130,4 +119,4 @@ const Hero = () => { ); }; -export default Hero; \ No newline at end of file +export default Hero; diff --git a/frontend/src/components/HowItWorks.tsx b/frontend/src/components/HowItWorks.tsx index 3e1373f..f6a4478 100644 --- a/frontend/src/components/HowItWorks.tsx +++ b/frontend/src/components/HowItWorks.tsx @@ -1,67 +1,57 @@ import React, { useEffect, useRef } from 'react'; -import { - Users, - ShieldCheck, - Vote, - Lock, - Check, - ArrowRight -} from 'lucide-react'; +import { Users, ShieldCheck, Vote, Lock, Check, ArrowRight } from 'lucide-react'; import { motion, useScroll, useTransform } from 'framer-motion'; +import { useTranslation } from 'react-i18next'; const steps = [ { icon: Users, - title: 'Voter Signup', - description: 'Simply click getstarted button and fill the form with your details. Once account is ready you can now login.', - color: 'from-vote-blue to-cyan-500' + key: 'signup', + color: 'from-vote-blue to-cyan-500', }, { icon: ShieldCheck, - title: 'Voter Authentication', - description: 'Email and facial recognition authentication ensures only eligible voters can access the system.', - color: 'from-vote-teal to-emerald-500' + key: 'authentication', + color: 'from-vote-teal to-emerald-500', }, { icon: Vote, - title: 'Secure Voting', - description: 'Cast your vote from any device with complete privacy protection.', - color: 'from-indigo-500 to-vote-blue' + key: 'voting', + color: 'from-indigo-500 to-vote-blue', }, { icon: Lock, - title: 'Blockchain Recording', - description: 'Your vote is cryptographically sealed and added to the Ethereum immutable blockchain.', - color: 'from-vote-blue to-vote-teal' + key: 'recording', + color: 'from-vote-blue to-vote-teal', }, { icon: Check, - title: 'Vote Verification', - description: 'Verify your vote was counted correctly while maintaining complete anonymity.', - color: 'from-vote-teal to-emerald-500' - } + key: 'verification', + color: 'from-vote-teal to-emerald-500', + }, ]; const HowItWorks = () => { + const { t } = useTranslation(); const containerRef = useRef(null); const { scrollYProgress } = useScroll({ target: containerRef, - offset: ["start end", "end start"] + offset: ['start end', 'end start'], }); - const progressWidth = useTransform(scrollYProgress, [0, 1], ["0%", "100%"]); + const progressWidth = useTransform(scrollYProgress, [0, 1], ['0%', '100%']); return (
- - { className="mb-6" > - Simple Process + {t('howItWorks.badge')}

- How SmartVote Works + {t('howItWorks.title')}

-

- Our streamlined process ensures secure, transparent, and accessible voting for everyone. -

+

{t('howItWorks.subtitle')}

{/* Progress Line */}
- -
- +
+
{steps.map((step, index) => (
- -

{step.title}

-

{step.description}

+

+ {t(`howItWorks.steps.${step.key}.title`)} +

+

{t(`howItWorks.steps.${step.key}.description`)}

- +
- {index < steps.length - 1 && ( - { )}
- +
{index % 2 !== 0 ? ( - -

{step.title}

-

{step.description}

+

+ {t(`howItWorks.steps.${step.key}.title`)} +

+

{t(`howItWorks.steps.${step.key}.description`)}

) : (
diff --git a/frontend/src/components/LanguageSwitcher.tsx b/frontend/src/components/LanguageSwitcher.tsx new file mode 100644 index 0000000..7a1620b --- /dev/null +++ b/frontend/src/components/LanguageSwitcher.tsx @@ -0,0 +1,78 @@ +import { useTranslation } from 'react-i18next'; +import { Globe } from 'lucide-react'; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; +import { Button } from '@/components/ui/button'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu'; + +const languages = [ + { code: 'en', name: 'English', flag: '๐Ÿ‡บ๐Ÿ‡ธ' }, + { code: 'tr', name: 'Tรผrkรงe', flag: '๐Ÿ‡น๐Ÿ‡ท' }, +]; + +export const LanguageSwitcher = () => { + const { i18n } = useTranslation(); + + const currentLanguage = languages.find((lang) => lang.code === i18n.language) || languages[0]; + + const handleLanguageChange = (langCode: string) => { + i18n.changeLanguage(langCode); + }; + + return ( + + + + + + {languages.map((language) => ( + handleLanguageChange(language.code)} + className={`gap-2 ${i18n.language === language.code ? 'bg-accent' : ''}`} + > + {language.flag} + {language.name} + + ))} + + + ); +}; + +export const LanguageSelect = () => { + const { i18n } = useTranslation(); + + const handleLanguageChange = (value: string) => { + i18n.changeLanguage(value); + }; + + return ( +
+ + +
+ ); +}; diff --git a/frontend/src/components/LoginNavbar.tsx b/frontend/src/components/LoginNavbar.tsx index 3931a27..4e340ab 100644 --- a/frontend/src/components/LoginNavbar.tsx +++ b/frontend/src/components/LoginNavbar.tsx @@ -2,10 +2,14 @@ import React from 'react'; import { Link } from 'react-router-dom'; import { Button } from './ui/button'; import { motion } from 'framer-motion'; +import { useTranslation } from 'react-i18next'; +import { LanguageSwitcher } from './LanguageSwitcher'; const LoginNavbar = () => { + const { t } = useTranslation(); + return ( - {
- SmartVote - @@ -28,21 +32,21 @@ const LoginNavbar = () => { - - - - - +
+ + + + + + + +
); }; -export default LoginNavbar; \ No newline at end of file +export default LoginNavbar; diff --git a/frontend/src/components/Navbar.tsx b/frontend/src/components/Navbar.tsx index 5dc2c3e..c3d4831 100644 --- a/frontend/src/components/Navbar.tsx +++ b/frontend/src/components/Navbar.tsx @@ -3,10 +3,13 @@ import { Button } from './ui/button'; import { Menu, X } from 'lucide-react'; import { Link } from 'react-router-dom'; import { motion, AnimatePresence } from 'framer-motion'; +import { useTranslation } from 'react-i18next'; +import { LanguageSwitcher } from './LanguageSwitcher'; const Navbar = () => { const [isOpen, setIsOpen] = useState(false); const [scrolled, setScrolled] = useState(false); + const { t } = useTranslation(); useEffect(() => { const handleScroll = () => { @@ -16,27 +19,32 @@ const Navbar = () => { return () => window.removeEventListener('scroll', handleScroll); }, []); + const navItems = [ + { key: 'howItWorks', href: '#how-it-works' }, + { key: 'security', href: '#security' }, + { key: 'privacy', href: '/privacy' }, + { key: 'contact', href: '/contact' }, + ]; + return ( -
- SmartVote - @@ -46,47 +54,35 @@ const Navbar = () => { {/* Desktop Navigation */}
- {['Privacy', 'Contact'].map((item, index) => ( - - ( + + - {item} + {t(`navigation.${item.key}`)} ))} - - + + + + - - - + + @@ -99,10 +95,7 @@ const Navbar = () => { onClick={() => setIsOpen(!isOpen)} className="text-gray-600 hover:text-vote-blue focus:outline-none p-2 rounded-lg hover:bg-gray-100/50 transition-all duration-200" > - + {isOpen ? : } @@ -112,7 +105,7 @@ const Navbar = () => { {/* Mobile Navigation */} {isOpen && ( - { className="md:hidden overflow-hidden" >
- {['How It Works', 'Security', 'Privacy', 'Contact'].map((item, index) => ( + {navItems.map((item, index) => ( setIsOpen(false)} > - {item} + {t(`navigation.${item.key}`)} ))} - - +
+ +
setIsOpen(false)}> - setIsOpen(false)}>
@@ -168,4 +162,4 @@ const Navbar = () => { ); }; -export default Navbar; \ No newline at end of file +export default Navbar; diff --git a/frontend/src/components/PolicyNavbar.tsx b/frontend/src/components/PolicyNavbar.tsx index 60d8f07..27ee8fa 100644 --- a/frontend/src/components/PolicyNavbar.tsx +++ b/frontend/src/components/PolicyNavbar.tsx @@ -3,12 +3,15 @@ import { Link } from 'react-router-dom'; import { Button } from './ui/button'; import { motion } from 'framer-motion'; import MobileMenu from './MobileMenu'; +import { useTranslation } from 'react-i18next'; +import { LanguageSwitcher } from './LanguageSwitcher'; const PolicyNavbar = () => { const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false); + const { t } = useTranslation(); return ( - {
- SmartVote - @@ -33,45 +36,34 @@ const PolicyNavbar = () => { {/* Desktop Navigation */}
- + + + - - +
{/* Mobile Menu */} - +
); }; -export default PolicyNavbar; \ No newline at end of file +export default PolicyNavbar; diff --git a/frontend/src/components/Reviews.tsx b/frontend/src/components/Reviews.tsx index 9466049..94f1447 100644 --- a/frontend/src/components/Reviews.tsx +++ b/frontend/src/components/Reviews.tsx @@ -3,41 +3,38 @@ import { motion } from 'framer-motion'; import { Star, Quote } from 'lucide-react'; import { Button } from './ui/button'; import { ArrowRight } from 'lucide-react'; +import { useTranslation } from 'react-i18next'; const reviewsData = [ { - name: "Sarah Johnson", - role: "Election Commissioner", - content: "SmartVote has revolutionized our electoral process. The security and transparency it provides are unmatched.", + key: 'sarah', rating: 5, - color: "from-blue-500 to-cyan-500" + color: 'from-blue-500 to-cyan-500', }, { - name: "Michael Chen", - role: "City Council Member", - content: "Implementing SmartVote was seamless. Our voter turnout increased by 30% due to the easy-to-use interface.", + key: 'michael', rating: 5, - color: "from-vote-blue to-vote-teal" + color: 'from-vote-blue to-vote-teal', }, { - name: "Emily Rodriguez", - role: "University of Zimbabwe Student Body President", - content: "Perfect for student government elections. The real-time results feature saved us countless hours of manual counting.", + key: 'emily', rating: 5, - color: "from-indigo-500 to-vote-blue" - } + color: 'from-indigo-500 to-vote-blue', + }, ]; const Reviews = () => { + const { t } = useTranslation(); + const containerVariants = { hidden: { opacity: 0 }, visible: { opacity: 1, transition: { staggerChildren: 0.1, - delayChildren: 0.3 - } - } + delayChildren: 0.3, + }, + }, }; const itemVariants = { @@ -47,15 +44,15 @@ const Reviews = () => { y: 0, transition: { duration: 0.5, - ease: "easeOut" - } - } + ease: 'easeOut', + }, + }, }; return (
- { > - Testimonials + {t('reviews.badge')} - - Trusted by Election Officials + {t('reviews.title')} - - See what election officials and organizations say about their experience with SmartVote + + {t('reviews.subtitle')} - { className="grid grid-cols-1 md:grid-cols-3 gap-8" > {reviewsData.map((review, index) => ( - +
-
+
- +
- {[...Array(review.rating)].map((_, i) => ( - - ))} -
- -

{review.content}

- + {[...Array(review.rating)].map((_, i) => ( + + ))} +
+ +

{t(`reviews.reviews.${review.key}.content`)}

+
-

{review.name}

-

{review.role}

+

{t(`reviews.reviews.${review.key}.name`)}

+

{t(`reviews.reviews.${review.key}.role`)}

diff --git a/frontend/src/components/SignupNavbar.tsx b/frontend/src/components/SignupNavbar.tsx index a7da970..d2f7354 100644 --- a/frontend/src/components/SignupNavbar.tsx +++ b/frontend/src/components/SignupNavbar.tsx @@ -2,10 +2,14 @@ import React from 'react'; import { Link } from 'react-router-dom'; import { Button } from './ui/button'; import { motion } from 'framer-motion'; +import { useTranslation } from 'react-i18next'; +import { LanguageSwitcher } from './LanguageSwitcher'; const SignupNavbar = () => { + const { t } = useTranslation(); + return ( - {
- SmartVote - @@ -28,24 +32,24 @@ const SignupNavbar = () => { - - - - - +
+ + + + + + + +
); }; -export default SignupNavbar; \ No newline at end of file +export default SignupNavbar; diff --git a/frontend/src/components/WhoWeServe.tsx b/frontend/src/components/WhoWeServe.tsx index eb831bf..b131384 100644 --- a/frontend/src/components/WhoWeServe.tsx +++ b/frontend/src/components/WhoWeServe.tsx @@ -3,62 +3,59 @@ import { motion } from 'framer-motion'; import { Building, Users, Hospital, Film, Briefcase, GraduationCap, ArrowRight } from 'lucide-react'; import { Card, CardContent } from './ui/card'; import { Button } from './ui/button'; +import { useTranslation } from 'react-i18next'; const servicesData = [ { - title: "Corporate Organizations", - description: "Streamline board meetings, shareholder voting, and corporate governance with secure digital balloting.", + key: 'corporate', icon: Building, - image: "/organizations/c7e4de6a-e088-4235-88ad-878842033225.png", - color: "from-blue-500 to-cyan-500" + image: '/organizations/c7e4de6a-e088-4235-88ad-878842033225.png', + color: 'from-blue-500 to-cyan-500', }, { - title: "Entertainment Industry", - description: "Manage award voting, content selection, and member polling with complete integrity and transparency.", + key: 'entertainment', icon: Film, - image: "/organizations/1cbb65a1-23bb-4f1c-aa8b-390a16b54bf7.png", - color: "from-vote-blue to-vote-teal" + image: '/organizations/1cbb65a1-23bb-4f1c-aa8b-390a16b54bf7.png', + color: 'from-vote-blue to-vote-teal', }, { - title: "Healthcare Organizations", - description: "Enable secure voting for medical boards, hospital committees, and healthcare governance decisions.", + key: 'healthcare', icon: Hospital, - image: "/organizations/3add6a70-ff62-4e4f-8c5e-303a0c62d28d.png", - color: "from-indigo-500 to-vote-blue" + image: '/organizations/3add6a70-ff62-4e4f-8c5e-303a0c62d28d.png', + color: 'from-indigo-500 to-vote-blue', }, { - title: "Unions & Associations", - description: "Facilitate fair and transparent union elections and member voting processes with verifiable results.", + key: 'unions', icon: Users, - image: "/organizations/1fe10f78-6f4f-412c-b6d7-96f41833896c.png", - color: "from-vote-teal to-emerald-500" + image: '/organizations/1fe10f78-6f4f-412c-b6d7-96f41833896c.png', + color: 'from-vote-teal to-emerald-500', }, { - title: "Business Partnerships", - description: "Make critical business decisions with a secure and transparent voting platform built for collaboration.", + key: 'business', icon: Briefcase, - image: "/organizations/9a88b15b-14a4-4e2a-bca3-6f26f3413261.png", - color: "from-vote-blue to-cyan-500" + image: '/organizations/9a88b15b-14a4-4e2a-bca3-6f26f3413261.png', + color: 'from-vote-blue to-cyan-500', }, { - title: "Educational Institutions", - description: "Streamline student government elections, faculty decisions, and administrative voting processes.", + key: 'education', icon: GraduationCap, - image: "/organizations/c694b0bf-933a-4ece-81ff-45c448155f22.png", - color: "from-vote-teal to-emerald-500" - } + image: '/organizations/c694b0bf-933a-4ece-81ff-45c448155f22.png', + color: 'from-vote-teal to-emerald-500', + }, ]; const WhoWeServe = () => { + const { t } = useTranslation(); + const containerVariants = { hidden: { opacity: 0 }, visible: { opacity: 1, transition: { staggerChildren: 0.1, - delayChildren: 0.3 - } - } + delayChildren: 0.3, + }, + }, }; const itemVariants = { @@ -68,15 +65,15 @@ const WhoWeServe = () => { y: 0, transition: { duration: 0.5, - ease: "easeOut" - } - } + ease: 'easeOut', + }, + }, }; return (
- { > - Our Clients + {t('whoWeServe.badge')} - - Who We Serve + {t('whoWeServe.title')} - - Empowering organizations across sectors with secure and transparent voting solutions + + {t('whoWeServe.subtitle')} - { className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6" > {servicesData.map((service, index) => ( - + -
@@ -130,13 +119,15 @@ const WhoWeServe = () => {
-

{service.title}

-

{service.description}

- @@ -144,7 +135,6 @@ const WhoWeServe = () => { ))} -
); diff --git a/frontend/src/components/admin/AdminOverview.tsx b/frontend/src/components/admin/AdminOverview.tsx index bb2cd7a..a521349 100644 --- a/frontend/src/components/admin/AdminOverview.tsx +++ b/frontend/src/components/admin/AdminOverview.tsx @@ -1,30 +1,43 @@ import React from 'react'; -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; -import { Calendar, Users, CheckCircle2, Clock, TrendingUp, AlertCircle } from "lucide-react"; -import { Progress } from "@/components/ui/progress"; -import { useReadContract } from "thirdweb/react"; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Calendar, Users, CheckCircle2, Clock, TrendingUp, AlertCircle } from 'lucide-react'; +import { Progress } from '@/components/ui/progress'; +import { useReadContract } from 'thirdweb/react'; import { singleElectionContract, electionFactoryContract } from '@/utils/thirdweb-client'; import { useAuth } from '@/auth/AuthProvider'; -import { PieChart, Pie, Cell, ResponsiveContainer, Legend, Tooltip, BarChart, Bar, XAxis, YAxis, CartesianGrid } from 'recharts'; -import { motion } from "framer-motion"; +import { + PieChart, + Pie, + Cell, + ResponsiveContainer, + Legend, + Tooltip, + BarChart, + Bar, + XAxis, + YAxis, + CartesianGrid, +} from 'recharts'; +import { motion } from 'framer-motion'; +import { useTranslation } from 'react-i18next'; // Map frontend states to contract states const stateMapping = { - 'upcoming': 0, // UPCOMING - 'active': 1, // ACTIVE - 'completed': 2 // COMPLETED + upcoming: 0, // UPCOMING + active: 1, // ACTIVE + completed: 2, // COMPLETED }; const COLORS = { - active: 'rgba(0, 136, 254, 0.9)', // Blue - upcoming: 'rgba(0, 196, 159, 0.9)', // Teal - completed: 'rgba(136, 132, 216, 0.9)' // Purple + active: 'rgba(0, 136, 254, 0.9)', // Blue + upcoming: 'rgba(0, 196, 159, 0.9)', // Teal + completed: 'rgba(136, 132, 216, 0.9)', // Purple }; const HOVER_COLORS = { - active: 'rgba(0, 136, 254, 1)', // Blue - upcoming: 'rgba(0, 196, 159, 1)', // Teal - completed: 'rgba(136, 132, 216, 1)' // Purple + active: 'rgba(0, 136, 254, 1)', // Blue + upcoming: 'rgba(0, 196, 159, 1)', // Teal + completed: 'rgba(136, 132, 216, 1)', // Purple }; interface ElectionData { @@ -39,37 +52,42 @@ interface ElectionData { export const AdminOverview = () => { const { user } = useAuth(); + const { t } = useTranslation(); // Get all elections by owner const { data: ownerElections, isPending: isOwnerPending } = useReadContract({ contract: electionFactoryContract, - method: "function getElectionsByOwner(address owner) view returns ((address electionAddress, uint256 id, string title, uint256 startTime, uint256 endTime, bool isPublic, address owner)[])", + method: + 'function getElectionsByOwner(address owner) view returns ((address electionAddress, uint256 id, string title, uint256 startTime, uint256 endTime, bool isPublic, address owner)[])', params: [user?.address], }); // Get elections by state const { data: activeElections, isPending: isActivePending } = useReadContract({ contract: electionFactoryContract, - method: "function getElectionsByState(uint8 _state) view returns ((address electionAddress, uint256 id, string title, uint256 startTime, uint256 endTime, bool isPublic, address owner)[])", + method: + 'function getElectionsByState(uint8 _state) view returns ((address electionAddress, uint256 id, string title, uint256 startTime, uint256 endTime, bool isPublic, address owner)[])', params: [stateMapping['active']], }); const { data: upcomingElections, isPending: isUpcomingPending } = useReadContract({ contract: electionFactoryContract, - method: "function getElectionsByState(uint8 _state) view returns ((address electionAddress, uint256 id, string title, uint256 startTime, uint256 endTime, bool isPublic, address owner)[])", + method: + 'function getElectionsByState(uint8 _state) view returns ((address electionAddress, uint256 id, string title, uint256 startTime, uint256 endTime, bool isPublic, address owner)[])', params: [stateMapping['upcoming']], }); const { data: completedElections, isPending: isCompletedPending } = useReadContract({ contract: electionFactoryContract, - method: "function getElectionsByState(uint8 _state) view returns ((address electionAddress, uint256 id, string title, uint256 startTime, uint256 endTime, bool isPublic, address owner)[])", + method: + 'function getElectionsByState(uint8 _state) view returns ((address electionAddress, uint256 id, string title, uint256 startTime, uint256 endTime, bool isPublic, address owner)[])', params: [stateMapping['completed']], }); // Filter elections by owner address const filterElectionsByOwner = (elections: readonly ElectionData[] | undefined) => { if (!elections || !user?.address) return []; - return elections.filter(election => election.owner.toLowerCase() === user.address.toLowerCase()); + return elections.filter((election) => election.owner.toLowerCase() === user.address.toLowerCase()); }; // Apply filters to get admin's elections @@ -81,13 +99,13 @@ export const AdminOverview = () => { const activeElectionsCount = adminActiveElections.length; const upcomingElectionsCount = adminUpcomingElections.length; const completedElectionsCount = adminCompletedElections.length; - const totalElections = (ownerElections?.length || 0); + const totalElections = ownerElections?.length || 0; // Calculate percentage distribution const distributionData = [ - { name: 'Active', value: activeElectionsCount }, - { name: 'Upcoming', value: upcomingElectionsCount }, - { name: 'Completed', value: completedElectionsCount }, + { name: t('adminOverview.activeElections'), value: activeElectionsCount }, + { name: t('adminOverview.upcomingElections'), value: upcomingElectionsCount }, + { name: t('adminOverview.completedElections'), value: completedElectionsCount }, ]; // Calculate average election duration @@ -124,7 +142,7 @@ export const AdminOverview = () => { if (!elections) return []; const now = Math.floor(Date.now() / 1000); const oneDay = 24 * 60 * 60; - return elections.filter(election => { + return elections.filter((election) => { const startTime = Number(election.startTime); return startTime > now && startTime <= now + oneDay; }); @@ -135,7 +153,7 @@ export const AdminOverview = () => { if (!elections) return []; const now = Math.floor(Date.now() / 1000); const oneDay = 24 * 60 * 60; - return elections.filter(election => { + return elections.filter((election) => { const endTime = Number(election.endTime); return endTime < now && endTime >= now - oneDay; }); @@ -163,13 +181,13 @@ export const AdminOverview = () => { - Active Elections + {t('adminOverview.activeElections')}
{activeElectionsCount}
-
Currently Running
+
{t('adminOverview.currentlyRunning')}
@@ -178,13 +196,13 @@ export const AdminOverview = () => { - Upcoming Elections + {t('adminOverview.upcomingElections')}
{upcomingElectionsCount}
-
Scheduled
+
{t('adminOverview.scheduled')}
@@ -193,13 +211,13 @@ export const AdminOverview = () => { - Completed Elections + {t('adminOverview.completedElections')}
{completedElectionsCount}
-
Finished
+
{t('adminOverview.finished')}
@@ -208,13 +226,13 @@ export const AdminOverview = () => { - Total Elections + {t('adminOverview.totalElections')}
{totalElections}
-
All Time
+
{t('adminOverview.allTime')}
@@ -225,7 +243,9 @@ export const AdminOverview = () => { {/* Election Distribution Pie Chart */} - Election Distribution + + {t('adminOverview.electionDistribution')} +
@@ -244,29 +264,29 @@ export const AdminOverview = () => { labelLine={true} > {distributionData.map((entry, index) => ( - ))} - [`${value} elections`, 'Count']} /> - ( - {value} - )} + formatter={(value: string) => {value}} /> @@ -277,31 +297,37 @@ export const AdminOverview = () => { {/* Election Duration Statistics */} - Election Duration Statistics + + {t('adminOverview.electionDurationStatistics')} +
-

Average Duration

+

{t('adminOverview.averageDuration')}

- {Math.round(averageDuration / (24 * 60 * 60))} days + {Math.round(averageDuration / (24 * 60 * 60))} {t('adminOverview.days')}

{longestElection && (
-

Longest Election

+

{t('adminOverview.longestElection')}

{longestElection.title}

- {Math.round((Number(longestElection.endTime) - Number(longestElection.startTime)) / (24 * 60 * 60))} days + {Math.round((Number(longestElection.endTime) - Number(longestElection.startTime)) / (24 * 60 * 60))}{' '} + {t('adminOverview.days')}

)} {shortestElection && (
-

Shortest Election

+

{t('adminOverview.shortestElection')}

{shortestElection.title}

- {Math.round((Number(shortestElection.endTime) - Number(shortestElection.startTime)) / (24 * 60 * 60))} days + {Math.round( + (Number(shortestElection.endTime) - Number(shortestElection.startTime)) / (24 * 60 * 60) + )}{' '} + {t('adminOverview.days')}

)} @@ -315,21 +341,26 @@ export const AdminOverview = () => { {/* Elections Starting Soon */} - Starting Soon + {t('adminOverview.startingSoon')}
{electionsStartingSoon.length > 0 ? ( electionsStartingSoon.map((election) => ( -
+

{election.title}

- Starts in {Math.round((Number(election.startTime) - Math.floor(Date.now() / 1000)) / 3600)} hours + {t('adminOverview.startsIn')}{' '} + {Math.round((Number(election.startTime) - Math.floor(Date.now() / 1000)) / 3600)}{' '} + {t('adminOverview.hours')}

)) ) : ( -

No elections starting soon

+

{t('adminOverview.noElectionsStartingSoon')}

)}
@@ -338,21 +369,28 @@ export const AdminOverview = () => { {/* Recently Completed */} - Recently Completed + + {t('adminOverview.recentlyCompleted')} +
{recentlyCompleted.length > 0 ? ( recentlyCompleted.map((election) => ( -
+

{election.title}

- Completed {Math.round((Math.floor(Date.now() / 1000) - Number(election.endTime)) / 3600)} hours ago + {t('adminOverview.completed')}{' '} + {Math.round((Math.floor(Date.now() / 1000) - Number(election.endTime)) / 3600)}{' '} + {t('adminOverview.hoursAgo')}

)) ) : ( -

No recently completed elections

+

{t('adminOverview.noRecentlyCompleted')}

)}
@@ -360,4 +398,4 @@ export const AdminOverview = () => {
); -}; \ No newline at end of file +}; diff --git a/frontend/src/components/admin/AdminSecurity.tsx b/frontend/src/components/admin/AdminSecurity.tsx new file mode 100644 index 0000000..8dbdd37 --- /dev/null +++ b/frontend/src/components/admin/AdminSecurity.tsx @@ -0,0 +1,405 @@ +import React, { useState, useEffect } from 'react'; +import { Tabs, TabsContent } from '@/components/ui/tabs'; +import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'; +import { Badge } from '@/components/ui/badge'; +import { securityService } from '@/api'; +import { calculateSecurityStats } from '@/utils/securityUtils'; +import { SecurityAuditLogsTable } from '../security/SecurityAuditLogsTable'; +import { SecurityBreachesTable } from '../security/SecurityBreachesTable'; +import { SecurityOverviewCards } from '../security/SecurityOverviewCards'; +import { SecurityAlertBanner } from '../security/SecurityAlertBanner'; +import { SecuritySearchHeader } from '../security/SecuritySearchHeader'; +import { SecurityRecentActivity } from '../security/SecurityRecentActivity'; +import { SecurityTrendsCard } from '../security/SecurityTrendsCard'; +import { useTranslation } from 'react-i18next'; + +export const AdminSecurity = () => { + const { t } = useTranslation(); + const [auditLogs, setAuditLogs] = useState([]); + const [breaches, setBreaches] = useState([]); + const [loading, setLoading] = useState(false); + const [selectedElection, setSelectedElection] = useState(''); + const [searchTerm, setSearchTerm] = useState(''); + const [statusFilter, setStatusFilter] = useState('all'); + const [timeFilter, setTimeFilter] = useState('24h'); + const [activeTab, setActiveTab] = useState('overview'); + + // Modal state for viewing details + const [selectedItem, setSelectedItem] = useState(null); + const [detailsModalOpen, setDetailsModalOpen] = useState(false); + + // Security statistics + const [securityStats, setSecurityStats] = useState({ + totalAudits: 0, + activeBreaches: 0, + resolvedBreaches: 0, + recentAudits: 0, + criticalBreaches: 0, + lastAuditTime: null, + }); + + // Fetch security data + const fetchSecurityData = async () => { + setLoading(true); + try { + // Fetch audit logs + const auditLogs = await securityService.getAuditLogs(); + setAuditLogs(auditLogs || []); + + // Fetch breaches + const breaches = await securityService.getBreaches(); + setBreaches(breaches || []); + + // Calculate statistics + const stats = calculateSecurityStats(auditLogs || [], breaches || []); + setSecurityStats(stats); + } catch (error) { + console.error('Error fetching security data:', error); + } finally { + setLoading(false); + } + }; + + // Mark breach as resolved + const markBreachResolved = async (breachId) => { + try { + await securityService.resolveBreach(breachId, true); + fetchSecurityData(); // Refresh data + } catch (error) { + console.error('Error updating breach:', error); + } + }; + + // Handle viewing details + const handleViewAuditDetails = (auditLog) => { + setSelectedItem({ + type: 'audit', + data: auditLog, + }); + setDetailsModalOpen(true); + }; + + const handleViewBreachDetails = (breach) => { + setSelectedItem({ + type: 'breach', + data: breach, + }); + setDetailsModalOpen(true); + }; + + // Close details modal + const closeDetailsModal = () => { + setDetailsModalOpen(false); + setSelectedItem(null); + }; + + // Filter functions + const filteredAuditLogs = auditLogs.filter((log) => { + const matchesSearch = + searchTerm === '' || + log.election_id?.toString().includes(searchTerm) || + log.details?.toLowerCase().includes(searchTerm.toLowerCase()); + + const matchesElection = selectedElection === '' || log.election_id?.toString() === selectedElection; + + return matchesSearch && matchesElection; + }); + + const filteredBreaches = breaches.filter((breach) => { + const matchesStatus = + statusFilter === 'all' || + (statusFilter === 'active' && !breach.resolved) || + (statusFilter === 'resolved' && breach.resolved); + + const matchesSearch = + searchTerm === '' || + breach.election_id?.toString().includes(searchTerm) || + breach.description?.toLowerCase().includes(searchTerm.toLowerCase()); + + return matchesStatus && matchesSearch; + }); + + useEffect(() => { + fetchSecurityData(); + }, []); + + // Parse audit details JSON safely + const parseAuditDetails = (detailsString) => { + try { + return JSON.parse(detailsString); + } catch (error) { + console.error('Failed to parse audit details:', error); + return null; + } + }; + + // Render details modal content + const renderDetailsContent = () => { + if (!selectedItem) return null; + + if (selectedItem.type === 'audit') { + const log = selectedItem.data; + const parsedDetails = log.details ? parseAuditDetails(log.details) : null; + + return ( +
+ {/* Basic Info */} +
+
+

{t('adminSecurity.electionId')}

+

Election {log.election_id}

+
+
+

{t('adminSecurity.checkTime')}

+

{new Date(log.check_time).toLocaleString()}

+
+
+ + {/* Vote Counts - only show if data exists */} + {(log.db_total_votes !== null || log.chain_total_votes !== null) && ( +
+
+

{t('adminSecurity.databaseVotes')}

+

{log.db_total_votes ?? 'N/A'}

+
+
+

{t('adminSecurity.blockchainVotes')}

+

+ {log.chain_total_votes ?? 'N/A'} +

+
+
+ )} + + {/* Status - only show if data exists */} + {(log.db_status || log.chain_status) && ( +
+
+

{t('adminSecurity.databaseStatus')}

+ {log.db_status || 'N/A'} +
+
+

{t('adminSecurity.blockchainStatus')}

+ + {log.chain_status || 'N/A'} + +
+
+ )} + + {/* Discrepancy Alert */} + {log.discrepancy_found && ( +
+

+ {t('adminSecurity.securityIssuesDetected')} +

+

+ {t('adminSecurity.securityIssuesDescription', { + count: parsedDetails?.summary?.failedChecks || 'multiple', + })} +

+
+ )} + + {/* Audit Summary */} + {parsedDetails?.summary && ( +
+

{t('adminSecurity.auditSummary')}

+
+
+ {t('adminSecurity.totalChecks')} +

{parsedDetails.summary.totalChecks}

+
+
+ {t('adminSecurity.passed')} +

{parsedDetails.summary.passedChecks}

+
+
+ {t('adminSecurity.failed')} +

{parsedDetails.summary.failedChecks}

+
+
+ {t('adminSecurity.passRate')} +

{Math.round(parsedDetails.summary.passRate)}%

+
+
+
+ )} + + {/* Security Violations */} + {parsedDetails?.violations && parsedDetails.violations.length > 0 && ( +
+

{t('adminSecurity.securityViolations')}

+
+ {parsedDetails.violations.map((violation, index) => ( +
+
+ + {violation.type.replace(/_/g, ' ').toUpperCase()} + +
+

{violation.message}

+ {violation.details?.error && ( +
+ {violation.details.error} +
+ )} +
+ ))} +
+
+ )} + + {/* Raw Details Fallback */} + {log.details && !parsedDetails && ( +
+

{t('adminSecurity.rawDetails')}

+
+ {log.details} +
+
+ )} +
+ ); + } + + if (selectedItem.type === 'breach') { + const breach = selectedItem.data; + const issueTypes = breach.issue_type ? breach.issue_type.split(',').map((type) => type.trim()) : []; + + return ( +
+ {/* Basic Info */} +
+
+

{t('adminSecurity.electionId')}

+

Election {breach.election_id}

+
+
+

{t('adminSecurity.detectedAt')}

+

{new Date(breach.detected_at).toLocaleString()}

+
+
+ + {/* Issue Types */} +
+

{t('adminSecurity.issueTypes')}

+
+ {issueTypes.map((type, index) => ( + + {type.replace(/_/g, ' ').toUpperCase()} + + ))} +
+
+ + {/* Status */} +
+

{t('adminSecurity.status')}

+ + {breach.resolved ? t('adminSecurity.resolved') : t('adminSecurity.active')} + + {breach.resolved && breach.resolved_at && ( +

+ {t('adminSecurity.resolvedAt', { date: new Date(breach.resolved_at).toLocaleString() })} +

+ )} +
+ + {/* Description - Parse individual issues */} +
+

{t('adminSecurity.issueDetails')}

+
+ {breach.description.split('; ').map((issue, index) => { + const [issueType, ...messageParts] = issue.split(': '); + const message = messageParts.join(': '); + + return ( +
+
+ {issueType.replace(/_/g, ' ').toUpperCase()} +
+

{message}

+
+ ); + })} +
+
+
+ ); + } + + return null; + }; + + return ( +
+ {/* Security Overview Cards */} + + + {/* Active Breaches Alert */} + + + {/* Main Security Tabs */} + + + + {/* Overview Tab */} + +
+ + +
+
+ + {/* Security Breaches Tab */} + + + + + {/* Audit Logs Tab */} + + + +
+ + {/* Details Modal */} + + + + + {selectedItem?.type === 'audit' + ? t('adminSecurity.auditLogDetails') + : t('adminSecurity.securityBreachDetails')} + + + {renderDetailsContent()} + + +
+ ); +}; + +export default AdminSecurity; diff --git a/frontend/src/components/admin/electionCreationForm/AdvancedSettingsStep.tsx b/frontend/src/components/admin/electionCreationForm/AdvancedSettingsStep.tsx index 560c274..e58b2b1 100644 --- a/frontend/src/components/admin/electionCreationForm/AdvancedSettingsStep.tsx +++ b/frontend/src/components/admin/electionCreationForm/AdvancedSettingsStep.tsx @@ -16,19 +16,17 @@ interface AdvancedSettingsStepProps { setBannerImageUrl: (url: string) => void; } -export const AdvancedSettingsStep = ({ - form, +export const AdvancedSettingsStep = ({ + form, prevStep, bannerImageUrl, - setBannerImageUrl + setBannerImageUrl, }: AdvancedSettingsStepProps) => { return (

Advanced Settings

-

- Customize the appearance and behavior of your election. -

+

Customize the appearance and behavior of your election.

@@ -56,11 +54,7 @@ export const AdvancedSettingsStep = ({ {bannerImageUrl ? (
- Banner Image + Banner Image
@@ -80,11 +74,7 @@ export const AdvancedSettingsStep = ({

No banner image uploaded

Upload a banner image to customize your election page

- +
)}
@@ -92,7 +82,8 @@ export const AdvancedSettingsStep = ({ - The banner image will be displayed at the top of your election page. Choose an image that represents your election well. + The banner image will be displayed at the top of your election page. Choose an image that represents + your election well.
@@ -100,22 +91,14 @@ export const AdvancedSettingsStep = ({
- - -
); -}; \ No newline at end of file +}; diff --git a/frontend/src/components/admin/electionCreationForm/BasicInfoStep.tsx b/frontend/src/components/admin/electionCreationForm/BasicInfoStep.tsx index 6f2c63d..3ed63ca 100644 --- a/frontend/src/components/admin/electionCreationForm/BasicInfoStep.tsx +++ b/frontend/src/components/admin/electionCreationForm/BasicInfoStep.tsx @@ -7,42 +7,57 @@ import ElectionDescriptionInput from '../../election/ElectionDescriptionInput'; import { Input } from '@/components/ui/input'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { DateTimePicker } from '../../election/DateTimePicker'; -import { Calendar as CalendarIcon, Globe, Lock, Plus, Trash, Upload, User, Users, AlertTriangle, Info } from 'lucide-react'; +import { + Calendar as CalendarIcon, + Globe, + Lock, + Plus, + Trash, + Upload, + User, + Users, + AlertTriangle, + Info, +} from 'lucide-react'; import { Switch } from '@/components/ui/switch'; import { useEffect } from 'react'; import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; import { z } from 'zod'; import { zodResolver } from '@hookform/resolvers/zod'; import { useToast } from '@/hooks/use-toast'; +import { useTranslation } from 'react-i18next'; // Define Zod schema for validation -const basicInfoSchema = z.object({ - title: z.string() - .min(5, 'Title must be at least 5 characters') - .max(100, 'Title cannot exceed 100 characters'), - description: z.string() - .min(20, 'Description must be at least 20 characters') - .max(1000, 'Description cannot exceed 1000 characters'), - rules: z.array(z.string()) - .min(1, 'At least one rule is required') - .refine((rules) => rules.every(rule => rule.trim().length > 0), 'Rules cannot be empty') - .refine((rules) => rules.every(rule => rule.length <= 200), 'Rules cannot exceed 200 characters'), - organization: z.string() - .min(1, 'Organization is required'), - startDate: z.date() - .refine((date) => { +const basicInfoSchema = z + .object({ + title: z.string().min(5, 'Title must be at least 5 characters').max(100, 'Title cannot exceed 100 characters'), + description: z + .string() + .min(20, 'Description must be at least 20 characters') + .max(1000, 'Description cannot exceed 1000 characters'), + rules: z + .array(z.string()) + .min(1, 'At least one rule is required') + .refine((rules) => rules.every((rule) => rule.trim().length > 0), 'Rules cannot be empty') + .refine((rules) => rules.every((rule) => rule.length <= 200), 'Rules cannot exceed 200 characters'), + organization: z.string().min(1, 'Organization is required'), + startDate: z.date().refine((date) => { const now = new Date(); const minStartDate = new Date(now.getTime() + 2 * 60 * 1000); // 2 minutes from now return date >= minStartDate; }, 'Start time must be at least 2 minutes ahead'), - endDate: z.date(), - isPublic: z.boolean() -}).refine((data) => { - return data.endDate > data.startDate; -}, { - message: 'End time must be after start time', - path: ['endDate'] -}); + endDate: z.date(), + isPublic: z.boolean(), + }) + .refine( + (data) => { + return data.endDate > data.startDate; + }, + { + message: 'End time must be after start time', + path: ['endDate'], + } + ); interface BasicInfoStepProps { form: ReturnType>; @@ -50,22 +65,20 @@ interface BasicInfoStepProps { } export const BasicInfoStep = ({ form, nextStep }: BasicInfoStepProps) => { + const { t } = useTranslation(); const { toast } = useToast(); - const { watch, trigger, formState: { errors } } = form; + const { + watch, + trigger, + formState: { errors }, + } = form; const startDate = watch('startDate'); const endDate = watch('endDate'); // Validate form before allowing next step const handleNext = async () => { - const isValid = await trigger([ - 'title', - 'description', - 'rules', - 'organization', - 'startDate', - 'endDate' - ]); - + const isValid = await trigger(['title', 'description', 'rules', 'organization', 'startDate', 'endDate']); + if (isValid) { nextStep(); } else { @@ -88,14 +101,14 @@ export const BasicInfoStep = ({ form, nextStep }: BasicInfoStepProps) => {
- Important Information + {t('electionCreation.basicInfo.importantInfo.title')}
    -
  • Election title must be between 5-100 characters
  • -
  • Description must be at least 20 characters long
  • -
  • At least one rule is required
  • -
  • Start time must be at least 2 minutes ahead of current time
  • -
  • End time must be after start time
  • + {t('electionCreation.basicInfo.importantInfo.requirements', { returnObjects: true }).map( + (requirement: string, index: number) => ( +
  • {requirement}
  • + ) + )}
@@ -105,66 +118,66 @@ export const BasicInfoStep = ({ form, nextStep }: BasicInfoStepProps) => { control={form.control} name="title" rules={{ - required: 'Election title is required', + required: t('electionCreation.basicInfo.fields.title.validation.required'), minLength: { value: 5, - message: 'Title must be at least 5 characters' + message: t('electionCreation.basicInfo.fields.title.validation.minLength'), }, maxLength: { value: 100, - message: 'Title cannot exceed 100 characters' - } + message: t('electionCreation.basicInfo.fields.title.validation.maxLength'), + }, }} render={({ field }) => ( - Election Title* + {t('electionCreation.basicInfo.fields.title.label')} - - A clear title helps voters identify the election. + {t('electionCreation.basicInfo.fields.title.description')} )} /> - ( - Organization* + {t('electionCreation.basicInfo.fields.organization.label')} @@ -173,19 +186,19 @@ export const BasicInfoStep = ({ form, nextStep }: BasicInfoStepProps) => { />
- - + { const start = new Date(form.getValues('startDate')); - return date < new Date(start.setHours(0, 0, 0, 0)); + return date < new Date(start.setHours(0, 0, 0, 0)); }} />
@@ -193,7 +206,7 @@ export const BasicInfoStep = ({ form, nextStep }: BasicInfoStepProps) => { {(form.formState.errors.startDate || form.formState.errors.endDate) && ( - Date Validation Error + {t('electionCreation.basicInfo.validation.dateError')} {form.formState.errors.startDate?.message || form.formState.errors.endDate?.message} @@ -210,19 +223,19 @@ export const BasicInfoStep = ({ form, nextStep }: BasicInfoStepProps) => { {field.value ? (
- Public Election + {t('electionCreation.basicInfo.fields.visibility.public.title')}
) : (
- Private Election + {t('electionCreation.basicInfo.fields.visibility.private.title')}
)} {field.value - ? 'Anyone can participate (with optional restrictions)' - : 'Only invited voters can participate (voter list required)'} + ? t('electionCreation.basicInfo.fields.visibility.public.description') + : t('electionCreation.basicInfo.fields.visibility.private.description')}
@@ -234,14 +247,11 @@ export const BasicInfoStep = ({ form, nextStep }: BasicInfoStepProps) => {
-
); -}; \ No newline at end of file +}; diff --git a/frontend/src/components/admin/electionCreationForm/CandidatesStep.tsx b/frontend/src/components/admin/electionCreationForm/CandidatesStep.tsx index 872157d..8215c30 100644 --- a/frontend/src/components/admin/electionCreationForm/CandidatesStep.tsx +++ b/frontend/src/components/admin/electionCreationForm/CandidatesStep.tsx @@ -31,7 +31,13 @@ interface CandidatesStepProps { export const CandidatesStep = ({ prevStep, nextStep }: CandidatesStepProps) => { const { toast } = useToast(); - const { watch, setValue, formState: { errors }, trigger, control } = useFormContext(); + const { + watch, + setValue, + formState: { errors }, + trigger, + control, + } = useFormContext(); const candidates = watch('candidates'); const handleAddCandidate = () => { @@ -98,27 +104,24 @@ export const CandidatesStep = ({ prevStep, nextStep }: CandidatesStepProps) => { {candidates.length === 0 && ( - - You need to add at least one candidate to proceed. - + You need to add at least one candidate to proceed. )}
{candidates.map((candidate, index) => ( - +
Candidate {index + 1} - {candidate.name && ( - - {candidate.name} - - )} + {candidate.name && {candidate.name}}