From 7af4ef1f4b29e9ebc8945d6a75cca159285fe2f2 Mon Sep 17 00:00:00 2001 From: Arend Peter Castelein Date: Fri, 11 Sep 2026 13:27:56 -0700 Subject: [PATCH 1/4] Fix @typescript-eslint/no-unused-vars, no-explicit-any, no-require-imports, and unused imports repo-wide MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Resolves #1593 (first pass). Fixes all four rules to zero violations across shared, backend, and frontend: - no-unused-vars: removed dead imports/locals; added a codebase-wide underscore-prefix ignore convention to eslint.base.mjs for params kept only for interface/signature conformance. - no-require-imports: converted require() to import across the app, except where require() is genuinely required (lazy-loaded deps, a pre-import sanity check, standalone CJS scripts) — those get a disable comment instead. - no-explicit-any: replaced with real types (Express Request/Response, library types where installed) or with proper narrowing; kept only where the underlying dependency has no usable types (multer, Kysely migrations, an unreconciled pg-boss API drift), each with a comment. - Removed a build/** blind spot in the frontend eslint config that was linting the compiled bundle instead of source. Along the way, fixed two behavior bugs a code review caught in the no-explicit-any pass: a privilege-escalation gap where a missing JWT email claim could match an empty admin_ids/audit_ids/credential_ids entry, and an auth-check reorder in claimElectionController that changed behavior for an already-owning temp-id user. Also extracted a getErrorMessage/hasErrorCode helper (errorUtils.ts) to replace ~18 duplicated unknown-catch narrowings. Verified: tsc --noEmit, eslint, and the full test suite pass clean on all three packages after every batch of changes. Not fixed here (pre-existing, out of scope for this pass, blocks the pre-commit hook's stricter --fix bar): no-var (158 instances), prefer-const (autofixed by the hook), and a handful of other rule categories across the files this touched — see follow-up. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_018SxX59NANLcLWzKZQ7dXSF --- eslint.base.mjs | 13 +++ .../Controllers/Ballot/castVoteController.ts | 35 ++++---- ...deleteAllBallotsForElectionIDController.ts | 2 +- ...AnonymizedBallotsByElectionIDController.ts | 7 +- .../Ballot/getBallotByBallotIDController.ts | 3 +- .../getBallotsByElectionIDController.ts | 2 +- .../Ballot/getWriteInNamesController.ts | 2 +- .../Election/archiveElectionController.ts | 4 +- .../Election/claimElectionController.ts | 10 +-- .../Election/createElectionController.ts | 6 +- .../Election/deleteElectionController.ts | 7 +- .../Election/editElectionController.ts | 3 +- .../Election/editElectionRolesController.ts | 5 +- .../Election/elections.controllers.ts | 31 ++++--- .../Election/finalizeElectionController.ts | 2 +- .../Election/getElectionHistoryController.ts | 2 +- .../Election/getElectionResultsController.ts | 2 +- .../Election/getElectionsController.ts | 10 +-- .../Controllers/Election/sandboxController.ts | 3 +- .../Election/sendEmailController.ts | 17 ++-- .../Election/sendInvitesController.ts | 28 ++++--- .../Election/setOpenStateController.ts | 2 +- .../Election/setPublicResultsController.ts | 4 +- .../Election/setWriteInResultsController.ts | 2 +- .../Roll/addElectionRollController.ts | 2 +- .../Roll/changeElectionRollController.ts | 15 ++-- .../Roll/clearElectionRollController.ts | 2 +- .../Roll/editElectionRollController.ts | 8 +- .../Roll/getElectionRollController.ts | 14 ++-- .../Roll/registerVoterController.ts | 2 +- .../Roll/revealVoterIdController.ts | 2 +- .../src/Controllers/Roll/voterRollUtils.ts | 10 +-- .../src/Controllers/User/auth.controllers.ts | 14 ++-- .../User/getUserTokenController.ts | 2 +- .../src/Controllers/controllerUtils.ts | 18 +--- .../Controllers/sendGridWebhookController.ts | 11 +-- .../src/Controllers/uploadImageController.ts | 11 +-- .../DevElections/elections/emailtracking.ts | 2 +- .../src/DevElections/makeDevElections.ts | 3 +- packages/backend/src/Express/index.d.ts | 7 +- packages/backend/src/IRequest.ts | 18 +++- .../src/Migrations/2023_07_03_Initial.ts | 2 + .../src/Migrations/2024_01_27_Create_Date.ts | 2 + .../Migrations/2024_01_29_pkeys_and_heads.ts | 2 + .../src/Migrations/2025_01_29_admin_upload.ts | 2 + .../src/Migrations/2026_03_19_email_events.ts | 2 + .../src/Migrations/2026_04_27_unique_head.ts | 2 + .../backend/src/Migrators/migrate-down.ts | 2 +- .../src/Migrators/migrate-to-latest.ts | 2 +- packages/backend/src/Migrators/migrate-up.ts | 2 +- .../backend/src/Migrators/migration-utils.ts | 1 - packages/backend/src/Models/Ballots.ts | 3 +- packages/backend/src/Models/ElectionRolls.ts | 18 ++-- packages/backend/src/Models/Elections.ts | 17 ++-- .../backend/src/Models/__mocks__/Ballots.ts | 20 ++--- .../src/Models/__mocks__/CastVoteStore.ts | 5 +- .../src/Models/__mocks__/ElectionRolls.ts | 14 ++-- .../backend/src/Models/__mocks__/Elections.ts | 10 +-- .../src/Models/__mocks__/EmailEvents.ts | 6 +- .../serialize-parameters-transformer.ts | 6 +- packages/backend/src/OpenApi/swaggerSpec.ts | 5 +- packages/backend/src/Routes/registerEvents.ts | 16 ++-- packages/backend/src/ServiceLocator.ts | 11 +-- .../src/Services/Account/AccountService.ts | 9 +- .../Services/Account/AccountServiceUtils.ts | 7 +- .../Account/__mocks__/AccountService.ts | 6 +- .../backend/src/Services/Blob/BlobService.ts | 5 +- .../Services/Blob/__mocks__/BlobService.ts | 4 +- .../src/Services/Email/EmailService.ts | 14 +++- .../src/Services/EventQueue/MockEventQueue.ts | 5 +- .../Services/EventQueue/PGBossEventQueue.ts | 11 ++- .../backend/src/Services/Logging/ILogger.ts | 8 +- .../backend/src/Services/Logging/Logger.ts | 10 +-- .../src/Services/Logging/LoggerImpl.ts | 11 ++- .../src/Services/Logging/LoggerMiddleware.ts | 3 +- .../src/Services/Logging/TestLoggerImpl.ts | 11 ++- .../backend/src/Tabulators/AllocatedScore.ts | 14 ++-- packages/backend/src/Tabulators/Approval.ts | 4 +- packages/backend/src/Tabulators/IRV.ts | 4 + .../backend/src/Tabulators/NoBallots.test.ts | 2 +- packages/backend/src/Tabulators/Plurality.ts | 6 +- .../backend/src/Tabulators/RankedRobin.ts | 2 +- packages/backend/src/Tabulators/Star.ts | 4 +- packages/backend/src/Tabulators/Util.ts | 82 ++----------------- .../src/Tabulators/VotingMethodSelecter.ts | 2 +- .../backend/src/Tabulators/testApproval.js | 3 +- .../backend/src/Tabulators/testPlurality.js | 3 +- packages/backend/src/Tabulators/tinyrand.ts | 1 - packages/backend/src/Util.ts | 10 +-- packages/backend/src/app.ts | 20 ++--- packages/backend/src/auth/MockUserStore.ts | 1 - .../src/auth/test/TestMockUserStore.ts | 2 +- packages/backend/src/errorCatchMiddleware.ts | 25 +++--- packages/backend/src/errorUtils.ts | 15 ++++ packages/backend/src/index.ts | 2 +- packages/backend/src/socketHandler.ts | 9 +- packages/backend/src/test/DBTest.ts | 7 +- packages/backend/src/test/DemoPGStore.ts | 16 ++-- packages/backend/src/test/EmailTest.js | 1 + packages/backend/src/test/TestHelper.ts | 45 ++++++---- .../backend/src/test/accountService.test.ts | 11 +-- .../src/test/anonymizedBallots.test.ts | 4 +- .../src/test/clearElectionRoll.test.ts | 2 +- .../backend/src/test/createElection.test.ts | 3 +- .../backend/src/test/customAuthKey.test.ts | 13 +-- packages/backend/src/test/database_sandbox.ts | 6 +- .../backend/src/test/editElection.test.ts | 3 +- packages/backend/src/test/emailRoll.test.ts | 4 +- .../backend/src/test/finalizeElection.test.ts | 3 +- packages/backend/src/test/idRoll.test.ts | 4 +- .../src/test/multiRaceElection.test.ts | 3 +- .../backend/src/test/multiRaceResults.test.ts | 6 +- .../backend/src/test/precinctElection.test.ts | 3 +- .../backend/src/test/sendGridWebhook.test.ts | 18 ++-- packages/backend/src/test/testInputs.ts | 4 +- packages/backend/src/test/writeIns.test.ts | 16 ++-- packages/backend/src/untyped-modules.d.ts | 6 ++ packages/backend/tsconfig.json | 7 +- packages/backend/verifyShared.js | 3 +- packages/frontend/eslint.config.js | 3 +- .../components/AuthSessionContextProvider.tsx | 2 +- .../src/components/Election/Admin/Admin.tsx | 1 - .../Election/Admin/PublishAndShare.tsx | 14 ++-- .../Election/ElectionStateWarning.tsx | 6 +- .../Election/Results/IRV/winner.tsx | 4 +- .../components/Election/Results/Results.tsx | 3 +- .../Results/STAR/STARDetailedResults.tsx | 1 - .../STAR/STAREqualPreferencesWidget.tsx | 1 - .../STAR/STARResultDetailedStepsWidget.tsx | 2 +- .../Results/STAR/STARResultSummaryWidget.tsx | 1 - .../Election/Results/ViewElectionResults.tsx | 3 +- .../Results/components/HeadToHeadWidget.tsx | 2 - .../Results/components/ResultsPieChart.tsx | 12 +-- .../Results/components/VoterIntentWidget.tsx | 2 +- .../src/components/Election/Sidebar.tsx | 4 +- .../Election/TemporaryAccessWarning.tsx | 2 +- .../Voting/DraggableIRVBallotView.tsx | 4 +- .../GenericBallotView/CandidateLabel.tsx | 4 +- .../components/Election/Voting/VotePage.tsx | 2 +- .../ElectionForm/Candidates/CandidateForm.tsx | 8 +- .../Details/ElectionDetailsInlineForm.tsx | 3 +- .../ElectionForm/Races/RaceDialog.tsx | 3 +- .../ElectionForm/Races/RaceForm.tsx | 26 +----- .../components/ElectionForm/Races/Races.tsx | 5 +- .../Races/VotingMethodSelector.tsx | 4 +- .../ElectionForm/Races/useEditRace.tsx | 9 +- .../ElectionForm/Wizard/WizardBasics.tsx | 2 +- .../src/components/Elections/QueryTool.tsx | 4 +- .../LandingPage/LandingPageCarousel.tsx | 3 +- .../LandingPage/LandingPageOtherTools.tsx | 5 +- .../src/components/NameMatchingTester.tsx | 3 +- packages/frontend/src/components/NavMenu.tsx | 1 - packages/frontend/src/hooks/useAPI.ts | 1 - packages/shared/fixSchemaRefs.ts | 2 +- packages/shared/src/domain_model/Ballot.ts | 7 +- packages/shared/src/domain_model/Election.ts | 6 +- .../shared/src/domain_model/ElectionRoll.ts | 9 +- .../src/domain_model/ElectionSettings.ts | 13 ++- packages/shared/src/domain_model/Race.ts | 8 -- packages/shared/src/domain_model/Util.ts | 7 +- .../shared/src/domain_model/permissions.ts | 2 +- packages/shared/src/utils/formatMarkdown.ts | 1 - packages/shared/src/utils/makeID.ts | 2 +- 163 files changed, 586 insertions(+), 617 deletions(-) create mode 100644 packages/backend/src/errorUtils.ts create mode 100644 packages/backend/src/untyped-modules.d.ts diff --git a/eslint.base.mjs b/eslint.base.mjs index 251bcb5bb..b3c168fdb 100644 --- a/eslint.base.mjs +++ b/eslint.base.mjs @@ -8,5 +8,18 @@ import { defineConfig } from "eslint/config"; export default defineConfig([ { files: ["**/*.{js,mjs,cjs,ts,jsx,tsx}"], plugins: { js }, extends: ["js/recommended"] }, tseslint.configs.recommended, + { + rules: { + // A leading underscore is this codebase's existing convention for "intentionally + // unused" (destructured-discard fields, params kept only for interface/signature + // conformance) — recognize it instead of flagging those as errors. + "@typescript-eslint/no-unused-vars": ["error", { + args: "after-used", + argsIgnorePattern: "^_", + varsIgnorePattern: "^_", + caughtErrorsIgnorePattern: "^_", + }], + }, + }, { ignores: ["**/node_modules/**"] }, ]); diff --git a/packages/backend/src/Controllers/Ballot/castVoteController.ts b/packages/backend/src/Controllers/Ballot/castVoteController.ts index 273cf4f42..e03671a6a 100644 --- a/packages/backend/src/Controllers/Ballot/castVoteController.ts +++ b/packages/backend/src/Controllers/Ballot/castVoteController.ts @@ -1,12 +1,10 @@ import { Election } from "@equal-vote/star-vote-shared/domain_model/Election"; -import { ElectionRoll } from "@equal-vote/star-vote-shared/domain_model/ElectionRoll"; import { Ballot, ballotValidation, NewBallot, OrderedNewBallot, RaceCandidateOrder } from '@equal-vote/star-vote-shared/domain_model/Ballot'; import ServiceLocator from "../../ServiceLocator"; import Logger from "../../Services/Logging/Logger"; import { BadRequest, Conflict, InternalServerError, Unauthorized } from "@curveball/http-errors"; import { ILoggingContext } from "../../Services/Logging/ILogger"; import { randomUUID } from "crypto"; -import { Uid } from "@equal-vote/star-vote-shared/domain_model/Uid"; import { Receipt } from "../../Services/Email/EmailTemplates" import { getOrCreateElectionRoll, checkForMissingAuthenticationData, getVoterAuthorization } from "../Roll/voterRollUtils" import { innerGetGlobalElectionStats } from "../Election"; @@ -18,6 +16,7 @@ import { expectPermission } from "../controllerUtils"; import { permissions } from "@equal-vote/star-vote-shared/domain_model/permissions"; import { OrderedVoteFormatError, orderedVotesToVotes } from "@equal-vote/star-vote-shared/domain_model/OrderedVoteCodec"; import { makeUniqueID, ID_LENGTHS, ID_PREFIXES } from "@equal-vote/star-vote-shared/utils/makeID"; +import { getErrorMessage } from '../../errorUtils'; const ElectionsModel = ServiceLocator.electionsDb(); const BallotModel = ServiceLocator.ballotsDb(); @@ -75,7 +74,7 @@ async function makeBallotEvent(req: IElectionRequest, targetElection: Election, if (targetElection.settings.ballot_updates && targetElection.state !== 'draft') { try { updatableBallot = await BallotModel.getBallotByVoterID(roll!.voter_id, inputBallot.election_id, req); - } catch(e: any) { + } catch(e: unknown) { const msg = "Error searching for prior ballot"; Logger.error(req, msg, e); throw new InternalServerError(msg); @@ -129,12 +128,12 @@ const mapOrderedNewBallot = (ballot: OrderedNewBallot, raceOrder: RaceCandidateO ...subBallot, votes: orderedVotesToVotes(orderedVotes, raceOrder) } - } catch (err: any) { + } catch (err: unknown) { if (err instanceof OrderedVoteFormatError) throw new BadRequest(err.message); throw err; } } -async function uploadBallotsController(req: IElectionRequest, res: Response, next: NextFunction) { +async function uploadBallotsController(req: IElectionRequest, res: Response, _next: NextFunction) { Logger.info(req, "Upload Ballots Controller"); expectPermission(req.user_auth.roles, permissions.canUploadBallots); @@ -184,19 +183,20 @@ async function uploadBallotsController(req: IElectionRequest, res: Response, nex `Admin submits a ballot for prior election` ) } else { - const validEvents = events.filter((event: any) => !('error' in event)) as CastVoteEvent[]; + const validEvents = events.filter((event) => !('error' in event)) as CastVoteEvent[]; const successfullySavedEvents: CastVoteEvent[] = []; for (const event of validEvents) { const ctx = Logger.createContext(event.requestId); try { await ServiceLocator.castVoteStore().submitBallotEvent(event, ctx); successfullySavedEvents.push(event); - } catch (e: any) { - Logger.error(req, `Could not upload ballot for ${event.roll?.voter_id || event.inputBallot.user_id || 'unknown'}: ${e.message}`); + } catch (e: unknown) { + const message = getErrorMessage(e); + Logger.error(req, `Could not upload ballot for ${event.roll?.voter_id || event.inputBallot.user_id || 'unknown'}: ${message}`); const index = events.indexOf(event); if (index !== -1) { output[index].success = false; - output[index].message = e.message; + output[index].message = message; } } } @@ -204,9 +204,9 @@ async function uploadBallotsController(req: IElectionRequest, res: Response, nex await (await EventQueue).publishBatch(castVoteEventQueue, successfullySavedEvents); } } - }catch(err: any){ + }catch(err: unknown){ const msg = `Could not upload ballots`; - Logger.error(req, `${msg}: ${err.message}`); + Logger.error(req, `${msg}: ${getErrorMessage(err)}`); throw new InternalServerError(msg) } @@ -218,7 +218,7 @@ async function uploadBallotsController(req: IElectionRequest, res: Response, nex Logger.debug(req, "CastVoteController done, saved event to store"); }; -async function castVoteController(req: IElectionRequest, res: Response, next: NextFunction) { +async function castVoteController(req: IElectionRequest, res: Response, _next: NextFunction) { Logger.info(req, "Cast Vote Controller"); const targetElection = req.election; @@ -240,13 +240,14 @@ async function castVoteController(req: IElectionRequest, res: Response, next: Ne const ctx = Logger.createContext(event.requestId); try { await ServiceLocator.castVoteStore().submitBallotEvent(event, ctx); - } catch (e: any) { - if (e.message === "ALREADY_VOTED") { + } catch (e: unknown) { + const message = getErrorMessage(e); + if (message === "ALREADY_VOTED") { Logger.info(req, "Ballot Rejected. User has already voted."); throw new BadRequest("User has already voted"); } - if (e.message === "CONCURRENT_BALLOT_UPDATE_DETECTED" || e.message === "CONCURRENT_ROLL_EDIT_DETECTED") { - Logger.info(req, `Ballot Rejected: ${e.message}`); + if (message === "CONCURRENT_BALLOT_UPDATE_DETECTED" || message === "CONCURRENT_ROLL_EDIT_DETECTED") { + Logger.info(req, `Ballot Rejected: ${message}`); throw new Conflict("Concurrent edit detected, please retry."); } throw e; @@ -287,7 +288,7 @@ async function handleCastVoteEvent(job: { id: string; data: CastVoteEvent; }):Pr } } -function assertVoterMayVote(voterAuthorization:any, election: Election, ctx:ILoggingContext ): void{ +function assertVoterMayVote(voterAuthorization: ReturnType, election: Election, ctx:ILoggingContext ): void{ Logger.debug(ctx, "assert voter may vote"); if (voterAuthorization.authorized_voter === false){ throw new Unauthorized("User not authorized to vote"); diff --git a/packages/backend/src/Controllers/Ballot/deleteAllBallotsForElectionIDController.ts b/packages/backend/src/Controllers/Ballot/deleteAllBallotsForElectionIDController.ts index b2c7f4571..1215b3b98 100644 --- a/packages/backend/src/Controllers/Ballot/deleteAllBallotsForElectionIDController.ts +++ b/packages/backend/src/Controllers/Ballot/deleteAllBallotsForElectionIDController.ts @@ -32,7 +32,7 @@ const innerDeleteAllBallotsForElectionID = async (req: IElectionRequest) => { return success } -const deleteAllBallotsForElectionID = async (req: IElectionRequest, res: Response, next: NextFunction) => { +const deleteAllBallotsForElectionID = async (req: IElectionRequest, res: Response, _next: NextFunction) => { res.json({ success: innerDeleteAllBallotsForElectionID(req) }) } diff --git a/packages/backend/src/Controllers/Ballot/getAnonymizedBallotsByElectionIDController.ts b/packages/backend/src/Controllers/Ballot/getAnonymizedBallotsByElectionIDController.ts index caa1976ff..eaa6c53f9 100644 --- a/packages/backend/src/Controllers/Ballot/getAnonymizedBallotsByElectionIDController.ts +++ b/packages/backend/src/Controllers/Ballot/getAnonymizedBallotsByElectionIDController.ts @@ -8,6 +8,7 @@ import { Response, NextFunction } from 'express'; import { AnonymizedBallot, Ballot } from "@equal-vote/star-vote-shared/domain_model/Ballot"; import { Readable } from 'stream'; import { pipeline } from 'stream/promises'; +import { hasErrorCode } from '../../errorUtils'; const BallotModel = ServiceLocator.ballotsDb(); @@ -35,7 +36,7 @@ async function* anonymizedBallotJsonChunks(ballots: AsyncIterable): Asyn yield buffer + ']}'; } -export const getAnonymizedBallotsByElectionID = async (req: IElectionRequest, res: Response, next: NextFunction) => { +export const getAnonymizedBallotsByElectionID = async (req: IElectionRequest, res: Response, _next: NextFunction) => { var electionId = req.election.election_id; Logger.debug(req, "getAnonymizedBallotsByElectionID: " + electionId); const election = req.election; @@ -59,8 +60,8 @@ export const getAnonymizedBallotsByElectionID = async (req: IElectionRequest, re // pipeline propagates backpressure (a slow client throttles the cursor) // and tears down the cursor if the client disconnects mid-stream. await pipeline(Readable.from(anonymizedBallotJsonChunks(ballots)), res); - } catch (err: any) { - if (err?.code === 'ERR_STREAM_PREMATURE_CLOSE') { + } catch (err: unknown) { + if (hasErrorCode(err, 'ERR_STREAM_PREMATURE_CLOSE')) { Logger.info(req, `getAnonymizedBallotsByElectionID: client disconnected mid-stream`); return; } diff --git a/packages/backend/src/Controllers/Ballot/getBallotByBallotIDController.ts b/packages/backend/src/Controllers/Ballot/getBallotByBallotIDController.ts index 5ef38ca25..990d9503d 100644 --- a/packages/backend/src/Controllers/Ballot/getBallotByBallotIDController.ts +++ b/packages/backend/src/Controllers/Ballot/getBallotByBallotIDController.ts @@ -1,13 +1,12 @@ import ServiceLocator from "../../ServiceLocator"; import Logger from "../../Services/Logging/Logger"; import { BadRequest } from "@curveball/http-errors"; -import { permissions } from '@equal-vote/star-vote-shared/domain_model/permissions'; import { IElectionRequest } from "../../IRequest"; import { Response, NextFunction } from 'express'; const BallotModel = ServiceLocator.ballotsDb(); -const getBallotByBallotID = async (req: IElectionRequest, res: Response, next: NextFunction) => { +const getBallotByBallotID = async (req: IElectionRequest, res: Response, _next: NextFunction) => { var electionId = req.election.election_id; var ballot_id = req.params.ballot_id if (!ballot_id) { diff --git a/packages/backend/src/Controllers/Ballot/getBallotsByElectionIDController.ts b/packages/backend/src/Controllers/Ballot/getBallotsByElectionIDController.ts index cbdc5748f..2ff3d6ee5 100644 --- a/packages/backend/src/Controllers/Ballot/getBallotsByElectionIDController.ts +++ b/packages/backend/src/Controllers/Ballot/getBallotsByElectionIDController.ts @@ -8,7 +8,7 @@ import { Response, NextFunction } from 'express'; const BallotModel = ServiceLocator.ballotsDb(); -const getBallotsByElectionID = async (req: IElectionRequest, res: Response, next: NextFunction) => { +const getBallotsByElectionID = async (req: IElectionRequest, res: Response, _next: NextFunction) => { var electionId = req.election.election_id; Logger.debug(req, "getBallotsByElectionID: " + electionId); diff --git a/packages/backend/src/Controllers/Ballot/getWriteInNamesController.ts b/packages/backend/src/Controllers/Ballot/getWriteInNamesController.ts index b8818350d..583fc6af2 100644 --- a/packages/backend/src/Controllers/Ballot/getWriteInNamesController.ts +++ b/packages/backend/src/Controllers/Ballot/getWriteInNamesController.ts @@ -9,7 +9,7 @@ import { WriteInData } from "@equal-vote/star-vote-shared/domain_model/WriteIn"; var BallotModel = ServiceLocator.ballotsDb(); -const getWriteInNamesController = async (req: IElectionRequest, res: Response, next: NextFunction) => { +const getWriteInNamesController = async (req: IElectionRequest, res: Response, _next: NextFunction) => { var electionId = req.election.election_id; Logger.debug(req, "getWriteInNames: " + electionId); diff --git a/packages/backend/src/Controllers/Election/archiveElectionController.ts b/packages/backend/src/Controllers/Election/archiveElectionController.ts index f8ca1ba11..65a61d099 100644 --- a/packages/backend/src/Controllers/Election/archiveElectionController.ts +++ b/packages/backend/src/Controllers/Election/archiveElectionController.ts @@ -2,7 +2,7 @@ import ServiceLocator from '../../ServiceLocator'; import Logger from '../../Services/Logging/Logger'; import { permissions } from '@equal-vote/star-vote-shared/domain_model/permissions'; import { expectPermission, expectUpdateDate } from "../controllerUtils"; -import { BadRequest, InternalServerError } from "@curveball/http-errors"; +import { BadRequest } from "@curveball/http-errors"; import { Election } from '@equal-vote/star-vote-shared/domain_model/Election'; import { IElectionRequest } from "../../IRequest"; import { Response, NextFunction } from 'express'; @@ -11,7 +11,7 @@ var ElectionsModel = ServiceLocator.electionsDb(); const className = "election.Controllers"; -const archiveElection = async (req: IElectionRequest, res: Response, next: NextFunction) => { +const archiveElection = async (req: IElectionRequest, res: Response, _next: NextFunction) => { Logger.info(req, `${className}.archive ${req.election.election_id}`); expectPermission(req.user_auth.roles, permissions.canEditElectionState) diff --git a/packages/backend/src/Controllers/Election/claimElectionController.ts b/packages/backend/src/Controllers/Election/claimElectionController.ts index a1b709eb8..4dc43568d 100644 --- a/packages/backend/src/Controllers/Election/claimElectionController.ts +++ b/packages/backend/src/Controllers/Election/claimElectionController.ts @@ -2,7 +2,7 @@ import ServiceLocator from '../../ServiceLocator'; import Logger from '../../Services/Logging/Logger'; import { permissions } from '@equal-vote/star-vote-shared/domain_model/permissions'; import { expectPermission, hashString } from "../controllerUtils"; -import { BadRequest, Unauthorized } from "@curveball/http-errors"; +import { Unauthorized } from "@curveball/http-errors"; import { IElectionRequest } from "../../IRequest"; import { Response, NextFunction } from 'express'; @@ -10,19 +10,19 @@ var ElectionsModel = ServiceLocator.electionsDb(); const className = "election.Controllers"; -const claimElection = async (req: IElectionRequest, res: Response, next: NextFunction) => { +const claimElection = async (req: IElectionRequest, res: Response, _next: NextFunction) => { Logger.info(req, `${className}.claimElection ${req.election.election_id}`); // temp_id will be verified against the election owner id to grant the owner role (even if we're logged in) expectPermission(req.user_auth.roles, permissions.canClaimElection) // check for no-op - if(req.election.owner_id == req.user.sub){ + if(req.election.owner_id == req.user?.sub){ res.send() return; } // must be logged in - if(req.user.typ != 'ID'){ + if(!req.user || req.user.typ != 'ID'){ throw new Unauthorized("User does not have permissions: must be logged in"); } @@ -34,7 +34,7 @@ const claimElection = async (req: IElectionRequest, res: Response, next: NextFun // Claim doesn't expose the election to the client beforehand, so OCC uses the // server's freshly-loaded copy as the expected version. const expected_update_date = req.election.update_date as string; - req.election.owner_id = req.user.sub; + req.election.owner_id = req.user.sub ?? null; await ElectionsModel.updateElection(req.election, req, `Transferring Ownership`, expected_update_date); res.send() diff --git a/packages/backend/src/Controllers/Election/createElectionController.ts b/packages/backend/src/Controllers/Election/createElectionController.ts index 6b9196ae1..b97c9fa59 100644 --- a/packages/backend/src/Controllers/Election/createElectionController.ts +++ b/packages/backend/src/Controllers/Election/createElectionController.ts @@ -1,18 +1,16 @@ import { Election, electionValidation } from "@equal-vote/star-vote-shared/domain_model/Election"; -import { ElectionRoll, ElectionRollState } from "@equal-vote/star-vote-shared/domain_model/ElectionRoll"; import { IRequest } from "../../IRequest"; import ServiceLocator from "../../ServiceLocator"; import Logger from "../../Services/Logging/Logger"; import { InternalServerError, BadRequest } from "@curveball/http-errors"; import { ILoggingContext } from "../../Services/Logging/ILogger"; -import { expectValidElectionFromRequest, catchAndRespondError, expectPermission } from "../controllerUtils"; +import { expectValidElectionFromRequest } from "../controllerUtils"; import { Response, NextFunction } from "express"; var ElectionsModel = ServiceLocator.electionsDb(); -const className = "createElectionController"; const failMsgPrfx = "CATCH: create error election err: "; -async function createElectionController(req: IRequest, res: Response, next: NextFunction) { +async function createElectionController(req: IRequest, res: Response, _next: NextFunction) { Logger.info(req, "Create Election Controller"); const inputElection = await expectValidElectionFromRequest(req); diff --git a/packages/backend/src/Controllers/Election/deleteElectionController.ts b/packages/backend/src/Controllers/Election/deleteElectionController.ts index 80e0e4c3e..431377a86 100644 --- a/packages/backend/src/Controllers/Election/deleteElectionController.ts +++ b/packages/backend/src/Controllers/Election/deleteElectionController.ts @@ -1,8 +1,6 @@ import ServiceLocator from '../../ServiceLocator'; import Logger from '../../Services/Logging/Logger'; -import { responseErr } from '../../Util'; -import { IRequest } from '../../IRequest'; -import { hasPermission, permissions } from '@equal-vote/star-vote-shared/domain_model/permissions'; +import { permissions } from '@equal-vote/star-vote-shared/domain_model/permissions'; import { expectPermission } from "../controllerUtils"; import { BadRequest } from "@curveball/http-errors"; import { IElectionRequest } from "../../IRequest"; @@ -11,11 +9,10 @@ import { Response, NextFunction } from 'express'; var ElectionsModel = ServiceLocator.electionsDb(); const className = "Elections.Controllers"; -const deleteElection = async (req: IElectionRequest, res: Response, next: NextFunction) => { +const deleteElection = async (req: IElectionRequest, res: Response, _next: NextFunction) => { expectPermission(req.user_auth.roles, permissions.canDeleteElection) const electionId = req.election.election_id; Logger.info(req, `${className}.deleteElection ${electionId}`) - var failMsg = "Election not deleted"; const success = await ElectionsModel.delete(electionId, req, `User manually deleting election`); if (!success) { var msg = "Nothing to delete"; diff --git a/packages/backend/src/Controllers/Election/editElectionController.ts b/packages/backend/src/Controllers/Election/editElectionController.ts index 49c3c469a..29e5a92c1 100644 --- a/packages/backend/src/Controllers/Election/editElectionController.ts +++ b/packages/backend/src/Controllers/Election/editElectionController.ts @@ -1,7 +1,6 @@ import { electionValidation } from '@equal-vote/star-vote-shared/domain_model/Election'; import ServiceLocator from '../../ServiceLocator'; import Logger from '../../Services/Logging/Logger'; -import { responseErr } from '../../Util'; import { expectPermission, expectUpdateDate } from "../controllerUtils"; import { permissions } from '@equal-vote/star-vote-shared/domain_model/permissions'; import { BadRequest } from "@curveball/http-errors"; @@ -11,7 +10,7 @@ import { Response, NextFunction } from 'express'; var ElectionsModel = ServiceLocator.electionsDb(); -const editElection = async (req: IElectionRequest, res: Response, next: NextFunction) => { +const editElection = async (req: IElectionRequest, res: Response, _next: NextFunction) => { const inputElection = req.body.Election; Logger.info(req, `editElection: ${inputElection?.election_id}`) expectPermission(req.user_auth.roles, permissions.canEditElection) diff --git a/packages/backend/src/Controllers/Election/editElectionRolesController.ts b/packages/backend/src/Controllers/Election/editElectionRolesController.ts index 51f1398a8..6433eff99 100644 --- a/packages/backend/src/Controllers/Election/editElectionRolesController.ts +++ b/packages/backend/src/Controllers/Election/editElectionRolesController.ts @@ -1,7 +1,5 @@ -import { electionValidation } from '@equal-vote/star-vote-shared/domain_model/Election'; import ServiceLocator from '../../ServiceLocator'; import Logger from '../../Services/Logging/Logger'; -import { responseErr } from '../../Util'; import { expectPermission, expectUpdateDate } from "../controllerUtils"; import { permissions } from '@equal-vote/star-vote-shared/domain_model/permissions'; import { BadRequest } from "@curveball/http-errors"; @@ -11,9 +9,8 @@ import { Response, NextFunction } from 'express'; var ElectionsModel = ServiceLocator.electionsDb(); -const editElectionRoles = async (req: IElectionRequest, res: Response, next: NextFunction) => { +const editElectionRoles = async (req: IElectionRequest, res: Response, _next: NextFunction) => { - const inputElection = req.body.Election; Logger.info(req, `editElectionRoles: ${req.election.election_id}`) expectPermission(req.user_auth.roles, permissions.canEditElectionRoles) // TODO: should this only be allowed in draft?? diff --git a/packages/backend/src/Controllers/Election/elections.controllers.ts b/packages/backend/src/Controllers/Election/elections.controllers.ts index 0a2f35026..43bfcdd93 100644 --- a/packages/backend/src/Controllers/Election/elections.controllers.ts +++ b/packages/backend/src/Controllers/Election/elections.controllers.ts @@ -2,7 +2,9 @@ import { Election, getPrecinctFilteredElection, removeHiddenFields } from '@equa import ServiceLocator from '../../ServiceLocator'; import Logger from '../../Services/Logging/Logger'; import { responseErr } from '../../Util'; +import { getErrorMessage } from '../../errorUtils'; import { IElectionRequest, IRequest } from '../../IRequest'; +import { Response, NextFunction } from 'express'; import { roles } from "@equal-vote/star-vote-shared/domain_model/roles" import { getPermissions } from '@equal-vote/star-vote-shared/domain_model/permissions'; import { getOrCreateElectionRoll, checkForMissingAuthenticationData, getVoterAuthorization } from "../Roll/voterRollUtils" @@ -15,31 +17,34 @@ var ElectionsModel = ServiceLocator.electionsDb(); var accountService = ServiceLocator.accountService(); const className="Elections.Controllers"; -const getElectionByID = async (req: any, res: any, next: any) => { +const getElectionByID = async (req: IElectionRequest, res: Response, next: NextFunction) => { Logger.info(req, `${className}.getElectionByID ${req.params.id}`); if (!req.params.id){ return next(); } try { let election = await ElectionsModel.getElectionByID(req.params.id, req); + if (!election) { + throw new Error(`Election not found: ${req.params.id}`); + } req.election = election; return next(); - } catch (err:any) { + } catch (err: unknown) { let failMsg = 'Election not found'; - Logger.error(req, `${failMsg} electionId=${req.params.id}}`); + Logger.error(req, `${failMsg} electionId=${req.params.id}: ${getErrorMessage(err)}`); return responseErr(res, req, 400, failMsg); } } -const electionExistsByID = async (req: any, res: any, next: any) => { +const electionExistsByID = async (req: IRequest, res: Response, _next: NextFunction) => { // using _id so that router.param() doesn't apply to it Logger.info(req, `${className}.getElectionExistsByID ${req.params._id}`); res.json({ exists: await ElectionsModel.electionExistsByID(req.params._id, req) }) } -const electionSpecificAuth = async (req: IElectionRequest, res: any, next: any) => { +const electionSpecificAuth = async (req: IElectionRequest, res: Response, next: NextFunction) => { if (!req.election){ return next(); } @@ -60,7 +65,7 @@ const electionSpecificAuth = async (req: IElectionRequest, res: any, next: any) return next(); } -const electionPostAuthMiddleware = async (req: IElectionRequest, res: any, next: any) => { +const electionPostAuthMiddleware = async (req: IElectionRequest, res: Response, next: NextFunction) => { Logger.info(req, `${className}.electionPostAuthMiddleware ${req.params.id}`); try { // Update Election State @@ -96,13 +101,13 @@ const electionPostAuthMiddleware = async (req: IElectionRequest, res: any, next: if((req.election.owner_id == req.user.sub && req.user.typ !== 'TEMP_ID') || tempUserAuth){ req.user_auth.roles.push(roles.owner) } - if (req.election.admin_ids && req.election.admin_ids.includes(req.user.email)){ + if (req.user.email && req.election.admin_ids && req.election.admin_ids.includes(req.user.email)){ req.user_auth.roles.push(roles.admin) } - if (req.election.audit_ids && req.election.audit_ids.includes(req.user.email)){ + if (req.user.email && req.election.audit_ids && req.election.audit_ids.includes(req.user.email)){ req.user_auth.roles.push(roles.auditor) } - if (req.election.credential_ids && req.election.credential_ids.includes(req.user.email)){ + if (req.user.email && req.election.credential_ids && req.election.credential_ids.includes(req.user.email)){ req.user_auth.roles.push(roles.credentialer) } } @@ -110,9 +115,9 @@ const electionPostAuthMiddleware = async (req: IElectionRequest, res: any, next: Logger.debug(req, `done with electionPostAuthMiddleware...`); Logger.debug(req,req.user_auth); return next(); - } catch (err:any) { + } catch (err: unknown) { var failMsg = "Could not modify election"; - Logger.error(req, `${failMsg} ${err.message}`); + Logger.error(req, `${failMsg} ${getErrorMessage(err)}`); return responseErr(res, req, 500, failMsg); } } @@ -157,7 +162,7 @@ async function updateElectionStateIfNeeded(req:IRequest, election:Election):Prom try { election = await ElectionsModel.updateElection(election, req, stateChangeMsg, expected_update_date); Logger.info(req, stateChangeMsg); - } catch (err: any) { + } catch (err: unknown) { // Concurrent GETs can both decide to transition state. Whichever loses // the OCC race re-reads to get the version that the winner installed. if (err instanceof Conflict) { @@ -171,7 +176,7 @@ async function updateElectionStateIfNeeded(req:IRequest, election:Election):Prom return election; } -const returnElection = async (req: any, res: any, next: any) => { +const returnElection = async (req: IElectionRequest, res: Response, _next: NextFunction) => { Logger.info(req, `${className}.returnElection ${req.params.id}`) var election = req.election; diff --git a/packages/backend/src/Controllers/Election/finalizeElectionController.ts b/packages/backend/src/Controllers/Election/finalizeElectionController.ts index e584d5095..c4134d991 100644 --- a/packages/backend/src/Controllers/Election/finalizeElectionController.ts +++ b/packages/backend/src/Controllers/Election/finalizeElectionController.ts @@ -13,7 +13,7 @@ var ElectionRollModel = ServiceLocator.electionRollDb(); const className = "election.Controllers"; -const finalizeElection = async (req: IElectionRequest, res: Response, next: NextFunction) => { +const finalizeElection = async (req: IElectionRequest, res: Response, _next: NextFunction) => { Logger.info(req, `${className}.finalize ${req.election.election_id}`); expectPermission(req.user_auth.roles, permissions.canEditElectionState) diff --git a/packages/backend/src/Controllers/Election/getElectionHistoryController.ts b/packages/backend/src/Controllers/Election/getElectionHistoryController.ts index a4d6425be..c701f84a2 100644 --- a/packages/backend/src/Controllers/Election/getElectionHistoryController.ts +++ b/packages/backend/src/Controllers/Election/getElectionHistoryController.ts @@ -217,7 +217,7 @@ export const buildHistory = ( return { finalizedAtMs, events }; }; -const getElectionHistory = async (req: IElectionRequest, res: Response, next: NextFunction) => { +const getElectionHistory = async (req: IElectionRequest, res: Response, _next: NextFunction) => { const electionId = req.election.election_id; Logger.info(req, `${className}.getElectionHistory ${electionId}`); diff --git a/packages/backend/src/Controllers/Election/getElectionResultsController.ts b/packages/backend/src/Controllers/Election/getElectionResultsController.ts index b3a58681f..091583cc1 100644 --- a/packages/backend/src/Controllers/Election/getElectionResultsController.ts +++ b/packages/backend/src/Controllers/Election/getElectionResultsController.ts @@ -12,7 +12,7 @@ import shuffleCandidatesForRandomTiebreak from "../../Tabulators/shuffleCandidat const BallotModel = ServiceLocator.ballotsDb(); -const getElectionResults = async (req: IElectionRequest, res: Response, next: NextFunction) => { +const getElectionResults = async (req: IElectionRequest, res: Response, _next: NextFunction) => { const election = req.election const electionId = election.election_id; diff --git a/packages/backend/src/Controllers/Election/getElectionsController.ts b/packages/backend/src/Controllers/Election/getElectionsController.ts index 11be360a9..69f428800 100644 --- a/packages/backend/src/Controllers/Election/getElectionsController.ts +++ b/packages/backend/src/Controllers/Election/getElectionsController.ts @@ -5,8 +5,6 @@ import { IElectionRequest, IRequest } from "../../IRequest"; import { Response, NextFunction } from 'express'; import { Election, removeHiddenFields } from '@equal-vote/star-vote-shared/domain_model/Election'; import { Race, VotingMethod, MethodTextKey, methodValueToTextKey } from '@equal-vote/star-vote-shared/domain_model/Race'; -import { expectPermission } from '../controllerUtils'; -import { permissions } from '@equal-vote/star-vote-shared/domain_model/permissions'; import { sharedConfig } from '@equal-vote/star-vote-shared/config'; @@ -14,7 +12,7 @@ var ElectionsModel = ServiceLocator.electionsDb(); var ElectionRollModel = ServiceLocator.electionRollDb(); // TODO: We should probably split this up as the user will only need one of these filters -const getElections = async (req: IElectionRequest, res: Response, next: NextFunction) => { +const getElections = async (req: IElectionRequest, res: Response, _next: NextFunction) => { Logger.info(req, `getElections`); // var filter = (req.query.filter == undefined) ? "" : req.query.filter; const email = req.user?.email || '' @@ -22,7 +20,7 @@ const getElections = async (req: IElectionRequest, res: Response, next: NextFunc /////////// ELECTIONS WE OWN //////////////// var elections_as_official = null; - if((email !== '' || id !== '') && req.user.typ != 'TEMP_ID'){ + if((email !== '' || id !== '') && req.user?.typ != 'TEMP_ID'){ elections_as_official = await ElectionsModel.getElections(id, email, req); if (!elections_as_official) { var msg = "Election does not exist"; @@ -69,7 +67,7 @@ const getElections = async (req: IElectionRequest, res: Response, next: NextFunc }); } -const queryElections = async (req: IElectionRequest, res: Response, next: NextFunction) => { +const queryElections = async (req: IElectionRequest, res: Response, _next: NextFunction) => { Logger.info(req, `queryElections`); // TODO: https://github.com/Equal-Vote/bettervoting/issues/976 @@ -163,7 +161,7 @@ const innerGetGlobalElectionStats = async (req: IRequest): Promise { +const getGlobalElectionStats = async (req: IRequest, res: Response, _next: NextFunction) => { res.json(innerGetGlobalElectionStats(req)); } diff --git a/packages/backend/src/Controllers/Election/sandboxController.ts b/packages/backend/src/Controllers/Election/sandboxController.ts index 0f4eb65e8..e3e9b9488 100644 --- a/packages/backend/src/Controllers/Election/sandboxController.ts +++ b/packages/backend/src/Controllers/Election/sandboxController.ts @@ -3,10 +3,9 @@ import Logger from '../../Services/Logging/Logger'; const className = "Elections.Controllers"; import { VotingMethods } from '../../Tabulators/VotingMethodSelecter' import { Request, Response, NextFunction } from 'express'; -import { STV } from '../../Tabulators/IRV'; import { VotingMethod } from '@equal-vote/star-vote-shared/domain_model/Race'; -const getSandboxResults = async (req: Request, res: Response, next: NextFunction) => { +const getSandboxResults = async (req: Request, res: Response, _next: NextFunction) => { Logger.info(req, `${className}.getSandboxResults`); const candidateNames = req.body.candidates; diff --git a/packages/backend/src/Controllers/Election/sendEmailController.ts b/packages/backend/src/Controllers/Election/sendEmailController.ts index ffb98d71f..766d85e16 100644 --- a/packages/backend/src/Controllers/Election/sendEmailController.ts +++ b/packages/backend/src/Controllers/Election/sendEmailController.ts @@ -12,6 +12,7 @@ import { IElectionRequest } from "../../IRequest"; import { Response, NextFunction } from 'express'; import { Imsg } from '../../Services/Email/IEmail'; import { logSafeHash } from '../../Services/Logging/logSafeHash'; +import { getErrorMessage } from '../../errorUtils'; var ElectionRollModel = ServiceLocator.electionRollDb(); var ElectionModel = ServiceLocator.electionsDb(); @@ -60,7 +61,7 @@ const makeTestRoll = (election_id: string, email: string) => { head: true } -const sendEmailsController = async (req: IElectionRequest, res: Response, next: NextFunction) => { +const sendEmailsController = async (req: IElectionRequest, res: Response, _next: NextFunction) => { Logger.info(req, `${className}.sendEmails ${req.election.election_id}`); expectPermission(req.user_auth.roles, permissions.canSendEmails) @@ -148,7 +149,7 @@ const sendEmailsController = async (req: IElectionRequest, res: Response, next: election: undefined, url: url, voter_id: roll.voter_id, - sender: req.user.email, + sender: req.user?.email ?? '', email: email_request.email, message_id: message_id, test_email: email_request.target == 'test' ? (roll.email ?? '') : '' @@ -159,9 +160,9 @@ const sendEmailsController = async (req: IElectionRequest, res: Response, next: var failMsg = "Failed to send invitations"; try { await (await EventQueue).publishBatch(SendEmailEventQueue, Jobs); - } catch (err: any) { + } catch (err: unknown) { const msg = `Could not send invitations`; - Logger.error(req, `${msg}: ${err.message}`); + Logger.error(req, `${msg}: ${getErrorMessage(err)}`); throw new InternalServerError(failMsg) } @@ -216,8 +217,8 @@ async function handleSendEmailEvent(job: { id: string; data: email_request_event event_timestamp: new Date().toISOString(), details: { status_code: emailResponse?.[0]?.[0]?.statusCode }, }, ctx); - } catch (err: any) { - Logger.error(ctx, `Could not insert email event: ${err.message}`); + } catch (err: unknown) { + Logger.error(ctx, `Could not insert email event: ${getErrorMessage(err)}`); } } @@ -240,9 +241,9 @@ async function handleSendEmailEvent(job: { id: string; data: email_request_event if (!updatedElectionRoll) { throw new InternalServerError() } - } catch (err: any) { + } catch (err: unknown) { const msg = `Could not update election roll`; - Logger.error(ctx, `${msg}: ${err.message}`); + Logger.error(ctx, `${msg}: ${getErrorMessage(err)}`); throw new InternalServerError(msg) } } diff --git a/packages/backend/src/Controllers/Election/sendInvitesController.ts b/packages/backend/src/Controllers/Election/sendInvitesController.ts index 88949b9f6..82003d6cf 100644 --- a/packages/backend/src/Controllers/Election/sendInvitesController.ts +++ b/packages/backend/src/Controllers/Election/sendInvitesController.ts @@ -11,6 +11,8 @@ import { randomUUID } from "crypto"; import { IElectionRequest } from "../../IRequest"; import { Response, NextFunction } from 'express'; import { logSafeHash } from '../../Services/Logging/logSafeHash'; +import { ILoggingContext } from '../../Services/Logging/ILogger'; +import { getErrorMessage } from '../../errorUtils'; var ElectionRollModel = ServiceLocator.electionRollDb(); var EmailService = ServiceLocator.emailService(); @@ -29,7 +31,7 @@ export type SendInviteEvent = { sender: string, } -const sendInvitationsController = async (req: IElectionRequest, res: Response, next: NextFunction) => { +const sendInvitationsController = async (req: IElectionRequest, res: Response, _next: NextFunction) => { Logger.info(req, `${className}.sendInvitations ${req.election.election_id}`); expectPermission(req.user_auth.roles, permissions.canSendEmails) @@ -65,7 +67,7 @@ const sendInvitationsController = async (req: IElectionRequest, res: Response, n res.json({}) } -async function sendBatchEmailInvites(req: any, electionRoll: ElectionRoll[], election: Election) { +async function sendBatchEmailInvites(req: IElectionRequest, electionRoll: ElectionRoll[], election: Election) { const Jobs: SendInviteEvent[] = [] const reqId = req.contextId ? req.contextId : randomUUID(); const url = ServiceLocator.globalData().mainUrl; @@ -76,7 +78,7 @@ async function sendBatchEmailInvites(req: any, electionRoll: ElectionRoll[], ele election: election, url: url, electionRoll: roll, - sender: req.user.email + sender: req.user?.email ?? '' } ) }) @@ -85,14 +87,14 @@ async function sendBatchEmailInvites(req: any, electionRoll: ElectionRoll[], ele Logger.info(req, `${className}.sendInvitations`, { election_id: election.election_id }); try { await (await EventQueue).publishBatch(SendInviteEventQueue, Jobs); - } catch (err: any) { + } catch (err: unknown) { const msg = `Could not send invitations`; - Logger.error(req, `${msg}: ${err.message}`); + Logger.error(req, `${msg}: ${getErrorMessage(err)}`); throw new InternalServerError(failMsg) } } -const sendInvitationController = async (req: any, res: any, next: any) => { +const sendInvitationController = async (req: IElectionRequest, res: Response, _next: NextFunction) => { Logger.info(req, `${className}.sendInvite ${req.election.election_id} ${logSafeHash(req.params.voter_id)}`); expectPermission(req.user_auth.roles, permissions.canSendEmails) @@ -114,9 +116,9 @@ const sendInvitationController = async (req: any, res: any, next: any) => { throw new InternalServerError('Could not find voter'); } - const updatedElectionRoll = await sendInvitation(req, election, electionRoll, req.user.email, url) + const updatedElectionRoll = await sendInvitation(req, election, electionRoll, req.user?.email ?? '', url) - return res.status('200').json({electionRoll: updatedElectionRoll}) + res.status(200).json({electionRoll: updatedElectionRoll}) } async function handleSendInviteEvent(job: { id: string; data: SendInviteEvent; }): Promise { @@ -131,7 +133,7 @@ async function handleSendInviteEvent(job: { id: string; data: SendInviteEvent; } await sendInvitation(ctx, event.election, electionRoll, event.sender, event.url) } -async function sendInvitation(ctx: any, election:Election, electionRoll: ElectionRoll, sender: string, url: string) { +async function sendInvitation(ctx: ILoggingContext, election:Election, electionRoll: ElectionRoll, sender: string, url: string) { const invites = Invites(election, [electionRoll], url) const emailResponse = await EmailService.sendEmails(invites) if (!electionRoll.email_data) { @@ -159,8 +161,8 @@ async function sendInvitation(ctx: any, election:Election, electionRoll: Electio event_timestamp: new Date().toISOString(), details: { status_code: emailResponse?.[0]?.[0]?.statusCode }, }, ctx); - } catch (err: any) { - Logger.error(ctx, `Could not insert email event: ${err.message}`); + } catch (err: unknown) { + Logger.error(ctx, `Could not insert email event: ${getErrorMessage(err)}`); } } @@ -179,9 +181,9 @@ async function sendInvitation(ctx: any, election:Election, electionRoll: Electio } else { throw new InternalServerError() } - } catch (err: any) { + } catch (err: unknown) { const msg = `Could not update election roll`; - Logger.error(ctx, `${msg}: ${err.message}`); + Logger.error(ctx, `${msg}: ${getErrorMessage(err)}`); throw new InternalServerError(msg) } } diff --git a/packages/backend/src/Controllers/Election/setOpenStateController.ts b/packages/backend/src/Controllers/Election/setOpenStateController.ts index 04d4c08d7..d20a697bd 100644 --- a/packages/backend/src/Controllers/Election/setOpenStateController.ts +++ b/packages/backend/src/Controllers/Election/setOpenStateController.ts @@ -11,7 +11,7 @@ const ElectionsModel = ServiceLocator.electionsDb(); const className = "election.Controllers"; -const setOpenState = async (req: IElectionRequest, res: Response, next: NextFunction) => { +const setOpenState = async (req: IElectionRequest, res: Response, _next: NextFunction) => { Logger.info(req, `${className}.archive ${req.election.election_id}`); expectPermission(req.user_auth.roles, permissions.canEditElectionState) diff --git a/packages/backend/src/Controllers/Election/setPublicResultsController.ts b/packages/backend/src/Controllers/Election/setPublicResultsController.ts index c5293fd60..5c0be6c43 100644 --- a/packages/backend/src/Controllers/Election/setPublicResultsController.ts +++ b/packages/backend/src/Controllers/Election/setPublicResultsController.ts @@ -2,7 +2,7 @@ import ServiceLocator from '../../ServiceLocator'; import Logger from '../../Services/Logging/Logger'; import { permissions } from '@equal-vote/star-vote-shared/domain_model/permissions'; import { expectPermission, expectUpdateDate } from "../controllerUtils"; -import { BadRequest, InternalServerError } from "@curveball/http-errors"; +import { BadRequest } from "@curveball/http-errors"; import { Election } from '@equal-vote/star-vote-shared/domain_model/Election'; import { IElectionRequest } from "../../IRequest"; import { Response, NextFunction } from 'express'; @@ -11,7 +11,7 @@ var ElectionsModel = ServiceLocator.electionsDb(); const className = "election.Controllers"; -const setPublicResults = async (req: IElectionRequest, res: Response, next: NextFunction) => { +const setPublicResults = async (req: IElectionRequest, res: Response, _next: NextFunction) => { Logger.info(req, `${className}.setPublicResults ${req.election.election_id}`); expectPermission(req.user_auth.roles, permissions.canEditElectionState) const election: Election = req.election diff --git a/packages/backend/src/Controllers/Election/setWriteInResultsController.ts b/packages/backend/src/Controllers/Election/setWriteInResultsController.ts index ebde661ff..e1706b997 100644 --- a/packages/backend/src/Controllers/Election/setWriteInResultsController.ts +++ b/packages/backend/src/Controllers/Election/setWriteInResultsController.ts @@ -52,7 +52,7 @@ function validateWriteInCandidates(candidates: unknown[]): WriteInCandidate[] { return result; } -const setWriteInResults = async (req: IElectionRequest, res: Response, next: NextFunction) => { +const setWriteInResults = async (req: IElectionRequest, res: Response, _next: NextFunction) => { Logger.info(req, `setWriteInResults ${req.election.election_id}`); expectPermission(req.user_auth.roles, permissions.canProcessWriteIns) diff --git a/packages/backend/src/Controllers/Roll/addElectionRollController.ts b/packages/backend/src/Controllers/Roll/addElectionRollController.ts index 3876aa874..afae90b0d 100644 --- a/packages/backend/src/Controllers/Roll/addElectionRollController.ts +++ b/packages/backend/src/Controllers/Roll/addElectionRollController.ts @@ -39,7 +39,7 @@ const addElectionRoll = async (req: IElectionRequest & { body: { electionRoll: E const history = [{ action_type: "added", - actor: req.user.email, + actor: req.user?.email ?? '', timestamp: Date.now(), }] if (req.election.settings.invitation === "email" && req.body.electionRoll.some((r: ElectionRollInput) => r.voter_id)) { diff --git a/packages/backend/src/Controllers/Roll/changeElectionRollController.ts b/packages/backend/src/Controllers/Roll/changeElectionRollController.ts index ab9c25839..11c3cc619 100644 --- a/packages/backend/src/Controllers/Roll/changeElectionRollController.ts +++ b/packages/backend/src/Controllers/Roll/changeElectionRollController.ts @@ -1,10 +1,9 @@ -import { ElectionRoll, ElectionRollState } from "@equal-vote/star-vote-shared/domain_model/ElectionRoll"; +import { ElectionRollState } from "@equal-vote/star-vote-shared/domain_model/ElectionRoll"; import ServiceLocator from "../../ServiceLocator"; import Logger from "../../Services/Logging/Logger"; -import { responseErr } from "../../Util"; const ElectionRollModel = ServiceLocator.electionRollDb(); -import { hasPermission, permission, permissions } from '@equal-vote/star-vote-shared/domain_model/permissions'; +import { permission, permissions } from '@equal-vote/star-vote-shared/domain_model/permissions'; import { expectPermission } from "../controllerUtils"; import { InternalServerError, Unauthorized } from "@curveball/http-errors"; import { IElectionRequest } from "../../IRequest"; @@ -12,25 +11,25 @@ import { Response, NextFunction } from 'express'; const className = "VoterRollState.Controllers"; -const approveElectionRoll = async (req: IElectionRequest, res: Response, next: NextFunction) => { +const approveElectionRoll = async (req: IElectionRequest, res: Response, _next: NextFunction) => { Logger.info(req, `${className}.approveElectionRoll ${req.params.id}`); await changeElectionRollState(req, ElectionRollState.approved, [ElectionRollState.registered, ElectionRollState.flagged], permissions.canApproveElectionRoll) res.status(200).json({}) } -const flagElectionRoll = async (req: IElectionRequest, res: Response, next: NextFunction) => { +const flagElectionRoll = async (req: IElectionRequest, res: Response, _next: NextFunction) => { Logger.info(req, `${className}.flagElectionRoll ${req.params.id}`); await changeElectionRollState(req, ElectionRollState.flagged, [ElectionRollState.approved, ElectionRollState.registered, ElectionRollState.invalid], permissions.canFlagElectionRoll) res.status(200).json({}) } -const invalidateElectionRoll = async (req: IElectionRequest, res: Response, next: NextFunction) => { +const invalidateElectionRoll = async (req: IElectionRequest, res: Response, _next: NextFunction) => { Logger.info(req, `${className}.flagElectionRoll ${req.params.id}`); await changeElectionRollState(req, ElectionRollState.invalid, [ElectionRollState.flagged], permissions.canInvalidateBallot) res.status(200).json({}) } -const uninvalidateElectionRoll = async (req: IElectionRequest, res: Response, next: NextFunction) => { +const uninvalidateElectionRoll = async (req: IElectionRequest, res: Response, _next: NextFunction) => { Logger.info(req, `${className}.flagElectionRoll ${req.params.id}`); await changeElectionRollState(req, ElectionRollState.flagged, [ElectionRollState.invalid], permissions.canInvalidateBallot) res.status(200).json({}) @@ -54,7 +53,7 @@ const changeElectionRollState = async (req: IElectionRequest, newState: Election } roll.history.push({ action_type: newState, - actor: req.user.email, + actor: req.user?.email ?? '', timestamp: Date.now(), }) const updatedEntry = await ElectionRollModel.update(roll, req, "Changing Election Roll state to " + newState); diff --git a/packages/backend/src/Controllers/Roll/clearElectionRollController.ts b/packages/backend/src/Controllers/Roll/clearElectionRollController.ts index 501348e4a..4586210aa 100644 --- a/packages/backend/src/Controllers/Roll/clearElectionRollController.ts +++ b/packages/backend/src/Controllers/Roll/clearElectionRollController.ts @@ -26,7 +26,7 @@ const clearElectionRoll = async (req: IElectionRequest, res: Response, next: Nex const cleared = await ElectionRollModel.archiveRollsByElectionID( req.election.election_id, req, - `${req.user.email} cleared the voter list of draft election ${req.election.election_id}` + `${req.user?.email ?? ''} cleared the voter list of draft election ${req.election.election_id}` ); Logger.info(req, `${className}.clearElectionRoll archived ${cleared} voters from ${req.election.election_id}`); diff --git a/packages/backend/src/Controllers/Roll/editElectionRollController.ts b/packages/backend/src/Controllers/Roll/editElectionRollController.ts index a965695de..8a466c833 100644 --- a/packages/backend/src/Controllers/Roll/editElectionRollController.ts +++ b/packages/backend/src/Controllers/Roll/editElectionRollController.ts @@ -1,8 +1,6 @@ -import { ElectionRoll, ElectionRollState } from "@equal-vote/star-vote-shared/domain_model/ElectionRoll"; import ServiceLocator from "../../ServiceLocator"; import Logger from "../../Services/Logging/Logger"; -import { responseErr } from "../../Util"; -import { hasPermission, permissions } from '@equal-vote/star-vote-shared/domain_model/permissions'; +import { permissions } from '@equal-vote/star-vote-shared/domain_model/permissions'; import { expectPermission } from "../controllerUtils"; import { BadRequest } from "@curveball/http-errors"; import { IElectionRequest } from "../../IRequest"; @@ -12,7 +10,7 @@ const ElectionRollModel = ServiceLocator.electionRollDb(); const className = "VoterRolls.Controllers"; -const editElectionRoll = async (req: IElectionRequest, res: Response, next: NextFunction) => { +const editElectionRoll = async (req: IElectionRequest, res: Response, _next: NextFunction) => { expectPermission(req.user_auth.roles, permissions.canEditElectionRoll) const electinoRollInput = req.body.electionRollEntry; Logger.info(req, `${className}.editElectionRoll election:${req.election.election_id}`); @@ -21,7 +19,7 @@ const editElectionRoll = async (req: IElectionRequest, res: Response, next: Next } electinoRollInput.history.push([{ action_type: 'edited', - actor: req.user.email, + actor: req.user?.email ?? '', timestamp: Date.now(), }]) const electionRollEntry = await ElectionRollModel.update(electinoRollInput, req, `User Editing Election Roll`); diff --git a/packages/backend/src/Controllers/Roll/getElectionRollController.ts b/packages/backend/src/Controllers/Roll/getElectionRollController.ts index ddbb31373..6330be482 100644 --- a/packages/backend/src/Controllers/Roll/getElectionRollController.ts +++ b/packages/backend/src/Controllers/Roll/getElectionRollController.ts @@ -5,9 +5,9 @@ import { expectPermission } from "../controllerUtils"; import { BadRequest, Unauthorized } from "@curveball/http-errors"; import { IElectionRequest } from "../../IRequest"; import { Response, NextFunction } from 'express'; -import { Election } from '@equal-vote/star-vote-shared/domain_model/Election'; import { ElectionRoll, ElectionRollAction, ElectionRollResponse } from '@equal-vote/star-vote-shared/domain_model/ElectionRoll'; import { logSafeHash } from '../../Services/Logging/logSafeHash'; +import { getErrorMessage } from '../../errorUtils'; const ElectionRollModel = ServiceLocator.electionRollDb(); const EmailEventsModel = ServiceLocator.emailEventsDb(); @@ -25,6 +25,7 @@ const redactString = (value: string, voterId: string | undefined, shouldRedact: // Note: ElectionRoll history entries can have nested structures and the email_data field is typed as 'any'. // This function uses a defensive approach to handle multiple data types (arrays, objects, strings), // strips out email_data entirely, and redacts voter IDs from action_type and actor fields. +/* eslint-disable @typescript-eslint/no-explicit-any -- defensive handling of genuinely unpredictable third-party/legacy shapes, see comments above and below */ const sanitizeHistory = ( history: ElectionRoll['history'], voterId: string | undefined, @@ -102,8 +103,9 @@ const sanitizeEmailMetadata = ( } return Object.keys(sanitized).length > 0 ? sanitized : undefined; } +/* eslint-enable @typescript-eslint/no-explicit-any */ -const getRollsByElectionID = async (req: IElectionRequest, res: Response, next: NextFunction) => { +const getRollsByElectionID = async (req: IElectionRequest, res: Response, _next: NextFunction) => { expectPermission(req.user_auth.roles, permissions.canViewElectionRoll) if(req.election.settings.voter_access === 'open'){ throw new Unauthorized("Can't view voter roll for open elections") @@ -133,8 +135,8 @@ const getRollsByElectionID = async (req: IElectionRequest, res: Response, next: details: event.details, }); } - } catch (err: any) { - Logger.warn(req, `Could not fetch email events: ${err.message}`); + } catch (err: unknown) { + Logger.warn(req, `Could not fetch email events: ${getErrorMessage(err)}`); } // Scrub ballot_id to prevent linking voters to ballots @@ -162,7 +164,7 @@ const getRollsByElectionID = async (req: IElectionRequest, res: Response, next: res.json({ election: req.election, electionRoll: scrubbedRoll }); } -const getByVoterID = async (req: IElectionRequest, res: Response, next: NextFunction) => { +const getByVoterID = async (req: IElectionRequest, res: Response, _next: NextFunction) => { Logger.info(req, `${className}.getByVoterID ${req.election.election_id} ${logSafeHash(req.params.voter_id)}`) const electionRollEntry = await ElectionRollModel.getByVoterID(req.election.election_id, req.params.voter_id, req) if (!electionRollEntry) { @@ -181,7 +183,7 @@ const getByVoterID = async (req: IElectionRequest, res: Response, next: NextFunc email_data: redactVoterIds ? sanitizeEmailMetadata(electionRollEntry.email_data, electionRollEntry.voter_id, redactVoterIds) : electionRollEntry.email_data }; if (redactVoterIds) { - delete (scrubbedEntry as any).voter_id; + delete (scrubbedEntry as Partial).voter_id; } res.json({ electionRollEntry: scrubbedEntry }) diff --git a/packages/backend/src/Controllers/Roll/registerVoterController.ts b/packages/backend/src/Controllers/Roll/registerVoterController.ts index 27f29fb5c..267cadad9 100644 --- a/packages/backend/src/Controllers/Roll/registerVoterController.ts +++ b/packages/backend/src/Controllers/Roll/registerVoterController.ts @@ -5,7 +5,7 @@ import { Response, NextFunction } from 'express'; const className = "VoterRolls.Controllers"; -const registerVoter = async (req: IElectionRequest, res: Response, next: NextFunction) => { +const registerVoter = async (req: IElectionRequest, _res: Response, _next: NextFunction) => { Logger.info(req, `${className}.registerVoter ${req.election?.election_id}`); // Reachable via POST /API/Election/:id/register, but no frontend code calls it diff --git a/packages/backend/src/Controllers/Roll/revealVoterIdController.ts b/packages/backend/src/Controllers/Roll/revealVoterIdController.ts index af0768dfe..1ed30d62e 100644 --- a/packages/backend/src/Controllers/Roll/revealVoterIdController.ts +++ b/packages/backend/src/Controllers/Roll/revealVoterIdController.ts @@ -22,7 +22,7 @@ const className = "VoterRolls.Controllers"; * This should only be used in emergency situations where an admin needs * to send a unique voting URL to a voter. */ -const revealVoterIdByEmail = async (req: IElectionRequest, res: Response, next: NextFunction) => { +const revealVoterIdByEmail = async (req: IElectionRequest, res: Response, _next: NextFunction) => { const electionId = req.election.election_id; const email = req.body.email; diff --git a/packages/backend/src/Controllers/Roll/voterRollUtils.ts b/packages/backend/src/Controllers/Roll/voterRollUtils.ts index 1b9760117..1a97408d1 100644 --- a/packages/backend/src/Controllers/Roll/voterRollUtils.ts +++ b/packages/backend/src/Controllers/Roll/voterRollUtils.ts @@ -18,7 +18,7 @@ export async function getOrCreateElectionRoll(req: IRequest, election: Election, // Get data that is used for voter authentication // NOTE: I'm ensuring that undefined is coaleced into null, that makes it compliant with the type when calling getElectionRoll const require_ip_hash = (election.settings.voter_authentication.ip_address ? ip_hash : null) ?? null; - const email = election.settings.voter_authentication.email ? req.user?.email : null + const email = election.settings.voter_authentication.email ? (req.user?.email ?? null) : null // Get voter ID if required and available, otherwise set to null let voter_id = null @@ -27,7 +27,7 @@ export async function getOrCreateElectionRoll(req: IRequest, election: Election, // https://help.vtex.com/en/tutorial/why-dont-cookies-support-special-characters--6hs7MQzTri6Yg2kQoSICoQ voter_id = voter_id_override ?? atob(req.cookies?.voter_id); } else if (election.settings.voter_authentication.voter_id && election.settings.voter_access == 'open') { - voter_id = voter_id_override ?? req.user?.sub + voter_id = voter_id_override ?? req.user?.sub ?? null } // Get all election roll entries that match any of the voter authentication fields @@ -44,8 +44,8 @@ export async function getOrCreateElectionRoll(req: IRequest, election: Election, if (!skipStateCheck && election.state !== 'open') return null Logger.info(req, "Creating new roll"); - const new_voter_id = election.settings.voter_authentication.voter_id ? - voter_id : + const new_voter_id = election.settings.voter_authentication.voter_id ? + (voter_id ?? '') : await makeUniqueID( ID_PREFIXES.VOTER, ID_LENGTHS.VOTER, @@ -96,7 +96,7 @@ export async function getOrCreateElectionRoll(req: IRequest, election: Election, Logger.error(req, `Email does not match saved election roll, voter: ${logSafeHash(electionRollEntries[0].voter_id)}`); throw new Unauthorized('Email does not match saved election roll'); } - if (election.settings.voter_authentication.voter_id && electionRollEntries[0].voter_id.trim() !== voter_id.trim()) { + if (election.settings.voter_authentication.voter_id && electionRollEntries[0].voter_id.trim() !== (voter_id ?? '').trim()) { // Voter ID does not match saved election roll, for example if email and voter ID are selected but email doesn't match the voter ID Logger.error(req, `Voter ID does not match saved election roll, voter: ${logSafeHash(electionRollEntries[0].voter_id)}`); throw new Unauthorized('Voter ID does not match saved voter roll'); diff --git a/packages/backend/src/Controllers/User/auth.controllers.ts b/packages/backend/src/Controllers/User/auth.controllers.ts index a151c2d46..f829e33a7 100644 --- a/packages/backend/src/Controllers/User/auth.controllers.ts +++ b/packages/backend/src/Controllers/User/auth.controllers.ts @@ -3,11 +3,13 @@ import { responseErr } from "../../Util" import { permission } from "@equal-vote/star-vote-shared/domain_model/permissions" import { roles } from "@equal-vote/star-vote-shared/domain_model/roles" import ServiceLocator from "../../ServiceLocator" +import { IElectionRequest, IRequest } from "../../IRequest" +import { Response, NextFunction } from 'express'; const className = 'Auth.Controllers'; const accountService = ServiceLocator.accountService(); -const getUser = (req: any, res: any, next: any) => { +const getUser = (req: IRequest, res: Response, next: NextFunction) => { Logger.info(req, `${className}.getUser`); const user = accountService.extractUserFromRequest(req); if (user){ @@ -17,7 +19,7 @@ const getUser = (req: any, res: any, next: any) => { } const hasPermission = (permission: permission) => { - return (req: any, res: any, next: any) => { + return (req: IElectionRequest, res: Response, next: NextFunction) => { Logger.debug(req, "\n= = = = =\n!!! hasPermission with: " + JSON.stringify(req.user_auth)); if (!req.user_auth.roles.some( (role:roles) => permission.includes(role))) { var msg = "Does not have permission"; @@ -28,7 +30,7 @@ const hasPermission = (permission: permission) => { } } -const isLoggedIn = (req: any, res: any, next: any) => { +const isLoggedIn = (req: IRequest, res: Response, next: NextFunction) => { Logger.info(req, `${className}.isLoggedIn user=${!!req.user}`); if (!req.user) { var msg = "Not Logged In"; @@ -38,10 +40,10 @@ const isLoggedIn = (req: any, res: any, next: any) => { next() } -const assertOwnership = (req: any, res: any, next: any) => { +const assertOwnership = (req: IElectionRequest, res: Response, next: NextFunction) => { Logger.info(req, `${className}.assertOwnership`); - Logger.debug(req, `${req.election.owner_id} ==? ${req.user.sub}`); - if (req.election.owner_id != req.user.sub) { + Logger.debug(req, `${req.election.owner_id} ==? ${req.user?.sub}`); + if (req.election.owner_id != req.user?.sub) { var msg = "Unauthorized: User does not own electon"; Logger.info(req, msg); return responseErr(res, req, 401, msg); diff --git a/packages/backend/src/Controllers/User/getUserTokenController.ts b/packages/backend/src/Controllers/User/getUserTokenController.ts index c49dc4396..d249b8e3a 100644 --- a/packages/backend/src/Controllers/User/getUserTokenController.ts +++ b/packages/backend/src/Controllers/User/getUserTokenController.ts @@ -3,7 +3,7 @@ import ServiceLocator from "../../ServiceLocator"; const AccountService = ServiceLocator.accountService() -const getUserToken = async (req: Request, res: Response, next: NextFunction) => { +const getUserToken = async (req: Request, res: Response, _next: NextFunction) => { const data = await AccountService.getToken(req) res.json(data) } diff --git a/packages/backend/src/Controllers/controllerUtils.ts b/packages/backend/src/Controllers/controllerUtils.ts index d5e7fece6..820c36996 100644 --- a/packages/backend/src/Controllers/controllerUtils.ts +++ b/packages/backend/src/Controllers/controllerUtils.ts @@ -1,8 +1,7 @@ -import { IRequest, reqIdSuffix } from "../IRequest" +import { IRequest } from "../IRequest" import { Election, electionValidation } from "@equal-vote/star-vote-shared/domain_model/Election"; import Logger from "../Services/Logging/Logger" import { BadRequest, Unauthorized } from "@curveball/http-errors"; -import { Response } from 'express'; import { roles } from "@equal-vote/star-vote-shared/domain_model/roles"; import { permission } from '@equal-vote/star-vote-shared/domain_model/permissions'; import { createHash, randomInt } from "crypto"; @@ -26,20 +25,7 @@ export async function expectValidElectionFromRequest(req:IRequest):Promise> { - var status = 500; - if (err.httpStatus) { - status = err.httpStatus; - } - var msg = "Error"; - if (err.detail) { - msg = err.detail; - } - msg += reqIdSuffix(req); - return res.status(status).json({error:msg}); -} - -export function expectPermission(roles:roles[],permission:permission):any { +export function expectPermission(roles:roles[],permission:permission): void { if (!roles.some( (role) => permission.includes(role))){ throw new Unauthorized("Does not have permission") } diff --git a/packages/backend/src/Controllers/sendGridWebhookController.ts b/packages/backend/src/Controllers/sendGridWebhookController.ts index 09a427b0d..61947e532 100644 --- a/packages/backend/src/Controllers/sendGridWebhookController.ts +++ b/packages/backend/src/Controllers/sendGridWebhookController.ts @@ -3,6 +3,7 @@ import crypto from 'crypto'; import { IRequest } from '../IRequest'; import Logger from '../Services/Logging/Logger'; import ServiceLocator from '../ServiceLocator'; +import { getErrorMessage } from '../errorUtils'; interface SendGridEvent { email?: string; @@ -77,7 +78,7 @@ export const sendGridWebhookController = async (req: IRequest, res: Response) => continue; } - const { email, unique_args, sg_message_id, event: event_type, timestamp: event_ts, ...rest } = event; + const { email: _email, unique_args: _unique_args, sg_message_id: _sg_message_id, event: event_type, timestamp: event_ts, ...rest } = event; await EmailEventsDB.insert({ message_id, election_id: sentRow.election_id, @@ -86,14 +87,14 @@ export const sendGridWebhookController = async (req: IRequest, res: Response) => event_timestamp: new Date((event_ts ?? Date.now() / 1000) * 1000).toISOString(), details: Object.keys(rest).length > 0 ? rest : undefined, }, req); - } catch (err: any) { - Logger.error(req, `SendGridWebhook: failed to store event for message_id=${message_id}: ${err.message}`); + } catch (err: unknown) { + Logger.error(req, `SendGridWebhook: failed to store event for message_id=${message_id}: ${getErrorMessage(err)}`); } } res.status(200).send('OK'); - } catch (err: any) { - Logger.error(req, `SendGridWebhook: unexpected error: ${err.message}`); + } catch (err: unknown) { + Logger.error(req, `SendGridWebhook: unexpected error: ${getErrorMessage(err)}`); res.status(500).send('Internal server error'); } }; diff --git a/packages/backend/src/Controllers/uploadImageController.ts b/packages/backend/src/Controllers/uploadImageController.ts index d3399123f..3d3a47d83 100644 --- a/packages/backend/src/Controllers/uploadImageController.ts +++ b/packages/backend/src/Controllers/uploadImageController.ts @@ -3,8 +3,9 @@ import Logger from '../Services/Logging/Logger'; import { randomUUID } from "crypto"; import { Request, Response, NextFunction } from 'express'; import ServiceLocator from "../ServiceLocator"; - -const multer = require("multer"); +import multer from "multer"; +import { getErrorMessage } from '../errorUtils'; +/* eslint-disable @typescript-eslint/no-explicit-any -- multer has no type declarations installed (@types/multer); revisit if that's added */ const storage = multer.memoryStorage(); @@ -31,7 +32,7 @@ interface ImageRequest extends Request { } // TODO: add multer file and S3 types -const uploadImageController = async (req: ImageRequest, res: Response, next: NextFunction) => { +const uploadImageController = async (req: ImageRequest, res: Response, _next: NextFunction) => { const file = req.file const blobName = `${randomUUID()}.jpg`; try { @@ -45,8 +46,8 @@ const uploadImageController = async (req: ImageRequest, res: Response, next: Nex Logger.info(req, `File uploaded successfully. ${photo_filename}`); res.json({ photo_filename }); - } catch (e: any) { - throw new InternalServerError(e); + } catch (e: unknown) { + throw new InternalServerError(getErrorMessage(e)); } } diff --git a/packages/backend/src/DevElections/elections/emailtracking.ts b/packages/backend/src/DevElections/elections/emailtracking.ts index 010195db5..520ab0694 100644 --- a/packages/backend/src/DevElections/elections/emailtracking.ts +++ b/packages/backend/src/DevElections/elections/emailtracking.ts @@ -57,7 +57,7 @@ const ballotPatterns: { voterIndex: number; scores: number[] }[] = [ ]; function makeBallots(): Ballot[] { - return ballotPatterns.map(({ voterIndex, scores }, i) => ({ + return ballotPatterns.map(({ voterIndex: _voterIndex, scores }, i) => ({ ballot_id: devBallotId(ELECTION_ID, i), election_id: ELECTION_ID, status: 'submitted', diff --git a/packages/backend/src/DevElections/makeDevElections.ts b/packages/backend/src/DevElections/makeDevElections.ts index 16071742c..ac4a21f47 100644 --- a/packages/backend/src/DevElections/makeDevElections.ts +++ b/packages/backend/src/DevElections/makeDevElections.ts @@ -1,5 +1,6 @@ import * as path from 'path' -require('dotenv').config({ path: path.resolve(__dirname, '../../.env') }) +import dotenv from 'dotenv' +dotenv.config({ path: path.resolve(__dirname, '../../.env') }) import servicelocator from '../ServiceLocator' import { DevElectionDefinition, validateDefinition } from './types' diff --git a/packages/backend/src/Express/index.d.ts b/packages/backend/src/Express/index.d.ts index 59f490ed3..f4301c8ad 100644 --- a/packages/backend/src/Express/index.d.ts +++ b/packages/backend/src/Express/index.d.ts @@ -1,6 +1,7 @@ import { Election } from '@equal-vote/star-vote-shared/domain_model/Election'; import { roles } from '@equal-vote/star-vote-shared/domain_model/roles'; -import { permission, permissions } from '@equal-vote/star-vote-shared/domain_model/permissions'; +import { permissions } from '@equal-vote/star-vote-shared/domain_model/permissions'; +import { IUser } from '../IRequest'; type p = keyof typeof permissions export {} @@ -12,13 +13,15 @@ declare global { contextId?: string; logPrefix?: string; election: Election; - user?: any; + user?: IUser; user_auth: { roles: roles[]; permissions: p[] } authorized_voter?: boolean; has_voted?: boolean; + // multer has no type declarations installed (@types/multer); revisit if that's added + // eslint-disable-next-line @typescript-eslint/no-explicit-any file: any } } diff --git a/packages/backend/src/IRequest.ts b/packages/backend/src/IRequest.ts index 3a711a048..1dfcbd76f 100644 --- a/packages/backend/src/IRequest.ts +++ b/packages/backend/src/IRequest.ts @@ -1,15 +1,25 @@ import { randomUUID } from 'crypto'; -import { Request } from 'express'; +import { Request, Response, NextFunction } from 'express'; import { Election } from '@equal-vote/star-vote-shared/domain_model/Election'; import { roles } from '@equal-vote/star-vote-shared/domain_model/roles'; -import { permission, permissions } from '@equal-vote/star-vote-shared/domain_model/permissions'; +import { permissions } from '@equal-vote/star-vote-shared/domain_model/permissions'; type p = keyof typeof permissions +// Shape of the decoded JWT / temp-id payload set on req.user. jsonwebtoken has no installed +// type declarations (see untyped-modules.d.ts), so this is asserted rather than inferred. +export interface IUser { + sub?: string; + email?: string; + typ?: string; + username?: string; + [key: string]: unknown; +} + export interface IRequest extends Request { contextId?: string; logPrefix?: string; - user?: any; + user?: IUser; } export interface IElectionRequest extends IRequest { @@ -27,7 +37,7 @@ export function reqIdSuffix(req:IRequest):string { return ` (${req.contextId})`; } -export function iRequestMiddleware(req:IRequest, _res:any, next:any):void { +export function iRequestMiddleware(req:IRequest, _res:Response, next:NextFunction):void { req.contextId = randomUUID().slice(0,8); next(); } \ No newline at end of file diff --git a/packages/backend/src/Migrations/2023_07_03_Initial.ts b/packages/backend/src/Migrations/2023_07_03_Initial.ts index fe9966ec0..0564620de 100644 --- a/packages/backend/src/Migrations/2023_07_03_Initial.ts +++ b/packages/backend/src/Migrations/2023_07_03_Initial.ts @@ -1,5 +1,6 @@ import { Kysely } from 'kysely' +// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Kysely's documented migration pattern: migrations must stay decoupled from the current (evolving) Database schema type export async function up(db: Kysely): Promise { await db.schema .createTable('electionDB') @@ -50,6 +51,7 @@ export async function up(db: Kysely): Promise { .execute() } +// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Kysely's documented migration pattern: migrations must stay decoupled from the current (evolving) Database schema type export async function down(db: Kysely): Promise { await db.schema.dropTable('electionDB').execute() await db.schema.dropTable('electionRollDB').execute() diff --git a/packages/backend/src/Migrations/2024_01_27_Create_Date.ts b/packages/backend/src/Migrations/2024_01_27_Create_Date.ts index 3ef392b89..595a78ba8 100644 --- a/packages/backend/src/Migrations/2024_01_27_Create_Date.ts +++ b/packages/backend/src/Migrations/2024_01_27_Create_Date.ts @@ -1,5 +1,6 @@ import { Kysely } from 'kysely' +// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Kysely's documented migration pattern: migrations must stay decoupled from the current (evolving) Database schema type export async function up(db: Kysely): Promise { await db.schema.alterTable('electionDB') .addColumn('claim_key_hash', 'varchar') @@ -27,6 +28,7 @@ export async function up(db: Kysely): Promise { .execute() } +// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Kysely's documented migration pattern: migrations must stay decoupled from the current (evolving) Database schema type export async function down(db: Kysely): Promise { await db.schema.alterTable('electionDB') .dropColumn('claim_key_hash') diff --git a/packages/backend/src/Migrations/2024_01_29_pkeys_and_heads.ts b/packages/backend/src/Migrations/2024_01_29_pkeys_and_heads.ts index a65509b7b..4df07b78b 100644 --- a/packages/backend/src/Migrations/2024_01_29_pkeys_and_heads.ts +++ b/packages/backend/src/Migrations/2024_01_29_pkeys_and_heads.ts @@ -1,5 +1,6 @@ import { Kysely } from 'kysely' +// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Kysely's documented migration pattern: migrations must stay decoupled from the current (evolving) Database schema type export async function up(db: Kysely): Promise { //Updating Election Rolls, adds new columns, default values, and changes primary key to set of election id, voter id, and update date await db.schema.alterTable('electionRollDB') @@ -95,6 +96,7 @@ export async function up(db: Kysely): Promise { .execute() } +// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Kysely's documented migration pattern: migrations must stay decoupled from the current (evolving) Database schema type export async function down(db: Kysely): Promise { // Election Rolls await db.schema.alterTable('electionRollDB') diff --git a/packages/backend/src/Migrations/2025_01_29_admin_upload.ts b/packages/backend/src/Migrations/2025_01_29_admin_upload.ts index d64d7db34..a549d8ad3 100644 --- a/packages/backend/src/Migrations/2025_01_29_admin_upload.ts +++ b/packages/backend/src/Migrations/2025_01_29_admin_upload.ts @@ -1,5 +1,6 @@ import { Kysely } from 'kysely' +// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Kysely's documented migration pattern: migrations must stay decoupled from the current (evolving) Database schema type export async function up(db: Kysely): Promise { await db.schema.alterTable('electionDB') /* ballot_source types @@ -22,6 +23,7 @@ export async function up(db: Kysely): Promise { .execute() } +// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Kysely's documented migration pattern: migrations must stay decoupled from the current (evolving) Database schema type export async function down(db: Kysely): Promise { await db.schema.alterTable('electionDB') .dropColumn('ballot_source') diff --git a/packages/backend/src/Migrations/2026_03_19_email_events.ts b/packages/backend/src/Migrations/2026_03_19_email_events.ts index 82611fbb1..40e78329e 100644 --- a/packages/backend/src/Migrations/2026_03_19_email_events.ts +++ b/packages/backend/src/Migrations/2026_03_19_email_events.ts @@ -1,5 +1,6 @@ import { Kysely } from 'kysely' +// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Kysely's documented migration pattern: migrations must stay decoupled from the current (evolving) Database schema type export async function up(db: Kysely): Promise { await db.schema .createTable('emailEventsDB') @@ -25,6 +26,7 @@ export async function up(db: Kysely): Promise { .execute() } +// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Kysely's documented migration pattern: migrations must stay decoupled from the current (evolving) Database schema type export async function down(db: Kysely): Promise { await db.schema.dropTable('emailEventsDB').execute() } diff --git a/packages/backend/src/Migrations/2026_04_27_unique_head.ts b/packages/backend/src/Migrations/2026_04_27_unique_head.ts index 560bdbb3c..64e8ed4d4 100644 --- a/packages/backend/src/Migrations/2026_04_27_unique_head.ts +++ b/packages/backend/src/Migrations/2026_04_27_unique_head.ts @@ -1,5 +1,6 @@ import { Kysely, sql } from 'kysely' +// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Kysely's documented migration pattern: migrations must stay decoupled from the current (evolving) Database schema type export async function up(db: Kysely): Promise { await db.schema.createIndex('electionDB_unique_head') .on('electionDB') @@ -23,6 +24,7 @@ export async function up(db: Kysely): Promise { .execute() } +// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Kysely's documented migration pattern: migrations must stay decoupled from the current (evolving) Database schema type export async function down(db: Kysely): Promise { await db.schema.dropIndex('electionRollDB_unique_head').execute() await db.schema.dropIndex('ballotDB_unique_head').execute() diff --git a/packages/backend/src/Migrators/migrate-down.ts b/packages/backend/src/Migrators/migrate-down.ts index 6661edd35..32174f788 100644 --- a/packages/backend/src/Migrators/migrate-down.ts +++ b/packages/backend/src/Migrators/migrate-down.ts @@ -1,4 +1,4 @@ -require('dotenv').config() +import 'dotenv/config' import { createMigrator, handleMigration } from "./migration-utils" async function migrateDown() { diff --git a/packages/backend/src/Migrators/migrate-to-latest.ts b/packages/backend/src/Migrators/migrate-to-latest.ts index 5fff456f0..d73e1e48b 100644 --- a/packages/backend/src/Migrators/migrate-to-latest.ts +++ b/packages/backend/src/Migrators/migrate-to-latest.ts @@ -1,4 +1,4 @@ -require('dotenv').config() +import 'dotenv/config' import { createMigrator, handleMigration } from "./migration-utils" async function migrateToLatest() { diff --git a/packages/backend/src/Migrators/migrate-up.ts b/packages/backend/src/Migrators/migrate-up.ts index b713a78a5..36ff32981 100644 --- a/packages/backend/src/Migrators/migrate-up.ts +++ b/packages/backend/src/Migrators/migrate-up.ts @@ -1,4 +1,4 @@ -require('dotenv').config() +import 'dotenv/config' import { createMigrator, handleMigration } from "./migration-utils" async function migrateUp() { diff --git a/packages/backend/src/Migrators/migration-utils.ts b/packages/backend/src/Migrators/migration-utils.ts index 3aaa9df09..c75a9b02d 100644 --- a/packages/backend/src/Migrators/migration-utils.ts +++ b/packages/backend/src/Migrators/migration-utils.ts @@ -1,6 +1,5 @@ import * as path from 'path' import { promises as fs } from 'fs' -import Logger from '../Services/Logging/Logger'; import { Migrator, FileMigrationProvider, diff --git a/packages/backend/src/Models/Ballots.ts b/packages/backend/src/Models/Ballots.ts index d3c08cae0..5f1a90951 100644 --- a/packages/backend/src/Models/Ballots.ts +++ b/packages/backend/src/Models/Ballots.ts @@ -6,7 +6,6 @@ import { logSafeHash } from '../Services/Logging/logSafeHash'; import { BallotVotes, IBallotStore } from './IBallotStore'; import { Kysely, sql, Transaction } from 'kysely'; import { Database } from './Database'; -import { InternalServerError } from '@curveball/http-errors'; const tableName = 'ballotDB'; const electionRollTableName = 'electionRollDB'; @@ -99,7 +98,7 @@ export default class BallotsDB implements IBallotStore { .where('ballot_id', '=', ballot_id) .where('head', '=', true) .executeTakeFirstOrThrow() - .catch((reason: any) => { + .catch((reason: unknown) => { Logger.debug(ctx, `${tableName}.get null`, reason); return null; }); diff --git a/packages/backend/src/Models/ElectionRolls.ts b/packages/backend/src/Models/ElectionRolls.ts index 36d085c9b..00398e9f1 100644 --- a/packages/backend/src/Models/ElectionRolls.ts +++ b/packages/backend/src/Models/ElectionRolls.ts @@ -2,7 +2,7 @@ import { ILoggingContext } from '../Services/Logging/ILogger'; import Logger from '../Services/Logging/Logger'; import { logSafeHash } from '../Services/Logging/logSafeHash'; import { IElectionRollStore } from './IElectionRollStore'; -import { Expression, Kysely, Transaction } from 'kysely' +import { Kysely, Transaction } from 'kysely' import { Database } from './Database'; import { ElectionRoll, NewElectionRoll } from '@equal-vote/star-vote-shared/domain_model/ElectionRoll'; const tableName = 'electionRollDB'; @@ -67,11 +67,11 @@ export default class ElectionRollDB implements IElectionRollStore { return this._postgresClient .selectFrom(tableName) .where('election_id', '=', election_id) - .where(({ eb, or, fn }) => eb(fn('trim', ['voter_id']), '=', voter_id.trim())) + .where(({ eb, or: _or, fn }) => eb(fn('trim', ['voter_id']), '=', voter_id.trim())) .where('head', '=', true) .selectAll() .executeTakeFirstOrThrow() - .catch(((reason: any) => { + .catch(((reason: unknown) => { Logger.debug(ctx, reason); return null })) @@ -85,7 +85,7 @@ export default class ElectionRollDB implements IElectionRollStore { .where('head', '=', true) .selectAll() .execute() - .catch(((reason: any) => { + .catch(((reason: unknown) => { Logger.debug(ctx, reason); return null })) @@ -101,7 +101,7 @@ export default class ElectionRollDB implements IElectionRollStore { .where('head', '=', true) .selectAll() .execute() - .catch(((reason: any) => { + .catch(((reason: unknown) => { Logger.debug(ctx, reason); return null })) @@ -117,7 +117,7 @@ export default class ElectionRollDB implements IElectionRollStore { .where('head', '=', true) .selectAll() .execute() - .catch(((reason: any) => { + .catch(((reason: unknown) => { Logger.debug(ctx, reason); return null })) @@ -151,7 +151,7 @@ export default class ElectionRollDB implements IElectionRollStore { if (rolls.length == 0) return null return rolls }) - .catch(((reason: any) => { + .catch(((reason: unknown) => { Logger.debug(ctx, reason); return null })) @@ -191,7 +191,7 @@ export default class ElectionRollDB implements IElectionRollStore { } else { return await this._postgresClient.transaction().execute(executeWork); } - } catch (reason: any) { + } catch (_reason: unknown) { Logger.debug(ctx, ".get null"); return null; } @@ -216,7 +216,7 @@ export default class ElectionRollDB implements IElectionRollStore { return archived.length } - delete(election_roll: ElectionRoll, ctx: ILoggingContext, reason: string): Promise { + delete(election_roll: ElectionRoll, ctx: ILoggingContext, _reason: string): Promise { Logger.debug(ctx, `${tableName}.delete`); var sqlString = `DELETE FROM ${this._tableName} WHERE election_id = $1 AND voter_id=$2`; Logger.debug(ctx, sqlString); diff --git a/packages/backend/src/Models/Elections.ts b/packages/backend/src/Models/Elections.ts index 05da46912..158e82e3c 100644 --- a/packages/backend/src/Models/Elections.ts +++ b/packages/backend/src/Models/Elections.ts @@ -8,6 +8,7 @@ import { sharedConfig } from '@equal-vote/star-vote-shared/config'; import { IElectionStore } from './IElectionStore'; import { Conflict, InternalServerError } from '@curveball/http-errors'; import { BadRequest } from "@curveball/http-errors"; +import { hasErrorCode } from '../errorUtils'; const tableName = 'electionDB'; @@ -16,8 +17,8 @@ interface IVoteCount{ v: number; } -const dneCatcher = (error: any) => { - if(error.code == '42P01'){ +const dneCatcher = (error: unknown) => { + if(hasErrorCode(error, '42P01')){ throw new InternalServerError(`${error} \n\n----------------------\n\nTables weren't created. Perhaps you need to run the migrate command? Try running the following...\n\n npm run build -w @equal-vote/star-vote-backend\n npm run migrate:latest -w @equal-vote/star-vote-backend\n\n\n`) } throw error; @@ -43,7 +44,7 @@ export default class ElectionsDB implements IElectionStore { return this._postgresClient.schema.dropTable(tableName).execute() } - createElection(election: Election, ctx: ILoggingContext, reason: string): Promise { + createElection(election: Election, ctx: ILoggingContext, _reason: string): Promise { Logger.debug(ctx, `${tableName}.createElection`, election); election.update_date = Date.now().toString()// Use now() because it doesn't change with time zone election.head = true @@ -105,7 +106,7 @@ export default class ElectionsDB implements IElectionStore { .execute() // // Filter for settings.voter_access = open - return openElections.filter((election: Election, index: any, array: any) => { + return openElections.filter((election: Election, _index: number, _array: Election[]) => { return election.settings.voter_access == 'open'; }); } @@ -239,8 +240,8 @@ export default class ElectionsDB implements IElectionStore { let content; try { content = await fetch(`${sharedConfig.CLASSIC_DOMAIN}/${election_id}`, {signal: controller.signal}) - .then((res:any) => res.text()) - .catch((err:any) => { + .then((res: Response) => res.text()) + .catch((err: unknown) => { Logger.error(ctx, 'error pinging star.vote', err) return errorMessage; }) @@ -267,7 +268,7 @@ export default class ElectionsDB implements IElectionStore { return elections } - delete(election_id: Uid, ctx: ILoggingContext, reason: string): Promise { + delete(election_id: Uid, ctx: ILoggingContext, _reason: string): Promise { Logger.debug(ctx, `${tableName}.delete ${election_id}`); const deletedElection = this._postgresClient @@ -286,7 +287,7 @@ export default class ElectionsDB implements IElectionStore { ) } - deleteAllElectionData(election_id: Uid, ctx: ILoggingContext, reason: string): Promise { + deleteAllElectionData(election_id: Uid, ctx: ILoggingContext, _reason: string): Promise { Logger.debug(ctx, `${tableName}.delete ${election_id}`); return this._postgresClient.transaction().execute(async (trx) => { diff --git a/packages/backend/src/Models/__mocks__/Ballots.ts b/packages/backend/src/Models/__mocks__/Ballots.ts index a8a412894..8becb26c9 100644 --- a/packages/backend/src/Models/__mocks__/Ballots.ts +++ b/packages/backend/src/Models/__mocks__/Ballots.ts @@ -7,25 +7,25 @@ export default class BallotsDB implements IBallotStore { ballots: Ballot[] = []; constructor() {} - submitBallot(ballot: Ballot, ctx:ILoggingContext, reason:string): Promise { + submitBallot(ballot: Ballot, _ctx:ILoggingContext, _reason:string): Promise { var copy = JSON.parse(JSON.stringify(ballot)); copy.head = true; // the real store always inserts ballots as the head version this.ballots.push(copy); return Promise.resolve(JSON.parse(JSON.stringify(copy))); } - updateBallot(ballot: Ballot, ctx:ILoggingContext, reason:string): Promise { + updateBallot(ballot: Ballot, _ctx:ILoggingContext, _reason:string): Promise { var copy = JSON.parse(JSON.stringify(ballot)); this.ballots.push(copy); return Promise.resolve(JSON.parse(JSON.stringify(copy))); } // place holder bulkSubmitBallots for now - bulkSubmitBallots(ballots: Ballot[], ctx:ILoggingContext, reason:string): Promise{ + bulkSubmitBallots(_ballots: Ballot[], _ctx:ILoggingContext, _reason:string): Promise{ return Promise.resolve(JSON.parse(JSON.stringify([] as Ballot[]))); } - getBallotsByElectionID(election_id: string, ctx:ILoggingContext): Promise { + getBallotsByElectionID(election_id: string, _ctx:ILoggingContext): Promise { const ballots = this.ballots.filter( (ballot) => ballot.election_id === election_id ); @@ -33,7 +33,7 @@ export default class BallotsDB implements IBallotStore { return Promise.resolve(resBallots); } - async *streamSubmittedBallotsByElectionID(election_id: string, ctx:ILoggingContext): AsyncIterableIterator { + async *streamSubmittedBallotsByElectionID(election_id: string, _ctx:ILoggingContext): AsyncIterableIterator { const ballots = this.ballots.filter( (ballot) => ballot.election_id === election_id && ballot.head && ballot.status === 'submitted' ); @@ -49,7 +49,7 @@ export default class BallotsDB implements IBallotStore { // mirrors getBallotsByElectionID's filter (see Ballots.ts) so tabulation // sees exactly the same ballots as the old non-streaming path - async *streamVotesByElectionID(election_id: string, ctx:ILoggingContext): AsyncIterableIterator { + async *streamVotesByElectionID(election_id: string, _ctx:ILoggingContext): AsyncIterableIterator { const ballots = this.ballots.filter( (ballot) => ballot.election_id === election_id ); @@ -58,7 +58,7 @@ export default class BallotsDB implements IBallotStore { } } - getBallotByVoterID(voter_id: string, election_id: string, ctx:ILoggingContext): Promise { + getBallotByVoterID(voter_id: string, _election_id: string, _ctx:ILoggingContext): Promise { const ballots = this.ballots.filter( (ballot) => ballot.user_id === voter_id ); @@ -69,7 +69,7 @@ export default class BallotsDB implements IBallotStore { return Promise.resolve(resBallots); } - getBallotByID(ballot_id: string, ctx:ILoggingContext): Promise { + getBallotByID(ballot_id: string, _ctx:ILoggingContext): Promise { const ballot = this.ballots.find( (ballot) => ballot.ballot_id === ballot_id ); @@ -80,11 +80,11 @@ export default class BallotsDB implements IBallotStore { return Promise.resolve(resBallot); } - deleteAllBallotsForElectionID(election_id: string, ctx: ILoggingContext): Promise { + deleteAllBallotsForElectionID(_election_id: string, _ctx: ILoggingContext): Promise { return Promise.resolve(true); } - delete(ballot_id: Uid, ctx:ILoggingContext,reason:string): Promise { + delete(ballot_id: Uid, _ctx:ILoggingContext,_reason:string): Promise { const ballot = this.ballots.find( (ballot) => ballot.ballot_id === ballot_id ); diff --git a/packages/backend/src/Models/__mocks__/CastVoteStore.ts b/packages/backend/src/Models/__mocks__/CastVoteStore.ts index 6dd64547f..0ee9d9ac4 100644 --- a/packages/backend/src/Models/__mocks__/CastVoteStore.ts +++ b/packages/backend/src/Models/__mocks__/CastVoteStore.ts @@ -1,8 +1,7 @@ -import { Ballot } from "@equal-vote/star-vote-shared/domain_model/Ballot"; -import { ElectionRoll } from "@equal-vote/star-vote-shared/domain_model/ElectionRoll"; import { ILoggingContext } from "../../Services/Logging/ILogger"; import { IBallotStore } from "../IBallotStore"; import { IElectionRollStore } from "../IElectionRollStore"; +import { CastVoteEvent } from "../CastVoteStore"; export default class CastVoteStore { @@ -14,7 +13,7 @@ export default class CastVoteStore { this._rollStore = rollStore; } - async submitBallotEvent(event: any, ctx: ILoggingContext): Promise { + async submitBallotEvent(event: CastVoteEvent, ctx: ILoggingContext): Promise { if (event.roll) { const currentRoll = await this._rollStore.getByVoterID(event.roll.election_id, event.roll.voter_id, ctx); if (currentRoll && currentRoll.submitted && !event.isBallotUpdate) { diff --git a/packages/backend/src/Models/__mocks__/ElectionRolls.ts b/packages/backend/src/Models/__mocks__/ElectionRolls.ts index a0a0e6838..74761aa77 100644 --- a/packages/backend/src/Models/__mocks__/ElectionRolls.ts +++ b/packages/backend/src/Models/__mocks__/ElectionRolls.ts @@ -1,7 +1,9 @@ -import { ElectionRoll, ElectionRollAction, ElectionRollState, NewElectionRoll } from '@equal-vote/star-vote-shared/domain_model/ElectionRoll'; +import { ElectionRoll, NewElectionRoll } from '@equal-vote/star-vote-shared/domain_model/ElectionRoll'; import { ILoggingContext } from '../../Services/Logging/ILogger'; import Logger from '../../Services/Logging/Logger'; import { IElectionRollStore } from '../IElectionRollStore'; +import { Kysely, Transaction } from 'kysely'; +import { Database } from '../Database'; export default class ElectionRollDB implements IElectionRollStore{ @@ -12,7 +14,7 @@ export default class ElectionRollDB implements IElectionRollStore{ this._electionRolls = []; } - submitElectionRoll(electionRolls: NewElectionRoll[], ctx:ILoggingContext, reason:string, db?: any): Promise { + submitElectionRoll(electionRolls: NewElectionRoll[], ctx:ILoggingContext, _reason:string, _db?: Kysely | Transaction): Promise { const self = this; const inserted: ElectionRoll[] = []; electionRolls.forEach(function(roll){ @@ -35,7 +37,7 @@ export default class ElectionRollDB implements IElectionRollStore{ return Promise.resolve(inserted); } - getRollsByElectionID(election_id: string, ctx:ILoggingContext): Promise { + getRollsByElectionID(election_id: string, _ctx:ILoggingContext): Promise { const electionRolls = this._electionRolls.filter(roll => roll.election_id===election_id && roll.head) if (!electionRolls){ return Promise.resolve(null) @@ -77,7 +79,7 @@ export default class ElectionRollDB implements IElectionRollStore{ return Promise.resolve(res) } - update(voter_roll: NewElectionRoll, ctx: ILoggingContext, reason: string, db?: any): Promise { + update(voter_roll: NewElectionRoll, ctx: ILoggingContext, _reason: string, _db?: Kysely | Transaction): Promise { Logger.debug(ctx, `MockElectionRolls update ${JSON.stringify(voter_roll)}`); const index = this._electionRolls.findIndex(electionRoll => { var electionMatch = electionRoll.election_id===voter_roll.election_id; @@ -97,7 +99,7 @@ export default class ElectionRollDB implements IElectionRollStore{ return Promise.resolve(JSON.parse(JSON.stringify(sanitized))); } - archiveRollsByElectionID(election_id: string, ctx:ILoggingContext, reason:string, db?: any): Promise { + archiveRollsByElectionID(election_id: string, ctx:ILoggingContext, _reason:string, _db?: Kysely | Transaction): Promise { Logger.debug(ctx, `MockElectionRolls archiveRollsByElectionID ${election_id}`); let archived = 0; this._electionRolls.forEach(roll => { @@ -109,7 +111,7 @@ export default class ElectionRollDB implements IElectionRollStore{ return Promise.resolve(archived) } - delete(voter_roll: ElectionRoll, ctx:ILoggingContext,reason:string): Promise { + delete(voter_roll: ElectionRoll, _ctx:ILoggingContext,_reason:string): Promise { const ballot = this._electionRolls.find(electionRoll => electionRoll.election_id===voter_roll.election_id && electionRoll.voter_id===voter_roll.voter_id) if (!ballot){ return Promise.resolve(false) diff --git a/packages/backend/src/Models/__mocks__/Elections.ts b/packages/backend/src/Models/__mocks__/Elections.ts index 3ed7bf6a9..61bc2c5fc 100644 --- a/packages/backend/src/Models/__mocks__/Elections.ts +++ b/packages/backend/src/Models/__mocks__/Elections.ts @@ -12,7 +12,7 @@ export default class ElectionsDB implements IElectionStore { constructor() { } - createElection(election: Election, ctx:ILoggingContext, reason:string): Promise{ + createElection(election: Election, ctx:ILoggingContext, _reason:string): Promise{ Logger.debug(ctx, "Election Mock Creates Election: ", election); var copy = JSON.parse(JSON.stringify(election)); copy.update_date = Date.now().toString(); @@ -37,14 +37,12 @@ export default class ElectionsDB implements IElectionStore { return Promise.resolve(res); } - getElections(id: string, email: string, ctx:ILoggingContext): Promise { + getElections(id: string, _email: string, _ctx:ILoggingContext): Promise { var elections:Array = JSON.parse(JSON.stringify(this.elections)); if(id != ""){ - var filters = id.split(','); - for(var i = 0; i < id.length; i++){ var [key, value] = id[i].split(':'); - elections = elections.filter(election => (election as any)[key]==String(value)) + elections = elections.filter(election => (election as unknown as Record)[key]==String(value)) } } if (!elections){ @@ -74,7 +72,7 @@ export default class ElectionsDB implements IElectionStore { return Promise.resolve(election? true : false); } - delete(election_id: Uid, ctx:ILoggingContext, reason:string): Promise { + delete(election_id: Uid, _ctx:ILoggingContext, _reason:string): Promise { const election = this.elections.find(election => election.election_id==election_id) if (!election){ return Promise.resolve(false) diff --git a/packages/backend/src/Models/__mocks__/EmailEvents.ts b/packages/backend/src/Models/__mocks__/EmailEvents.ts index e8d85d86d..12619d2f6 100644 --- a/packages/backend/src/Models/__mocks__/EmailEvents.ts +++ b/packages/backend/src/Models/__mocks__/EmailEvents.ts @@ -12,15 +12,15 @@ export default class EmailEventsDB { this._events.push({ ...event, id: this._nextId++ }); } - async getByElectionAndVoter(election_id: string, voter_id: string, ctx: ILoggingContext): Promise { + async getByElectionAndVoter(election_id: string, voter_id: string, _ctx: ILoggingContext): Promise { return this._events.filter(e => e.election_id === election_id && e.voter_id === voter_id); } - async getByElectionId(election_id: string, ctx: ILoggingContext): Promise { + async getByElectionId(election_id: string, _ctx: ILoggingContext): Promise { return this._events.filter(e => e.election_id === election_id); } - async getByMessageId(message_id: string, ctx: ILoggingContext): Promise { + async getByMessageId(message_id: string, _ctx: ILoggingContext): Promise { return this._events.find(e => e.message_id === message_id && e.event_type === 'sent') ?? null; } } diff --git a/packages/backend/src/Models/serialize-parameters/serialize-parameters-transformer.ts b/packages/backend/src/Models/serialize-parameters/serialize-parameters-transformer.ts index a37114812..509534158 100644 --- a/packages/backend/src/Models/serialize-parameters/serialize-parameters-transformer.ts +++ b/packages/backend/src/Models/serialize-parameters/serialize-parameters-transformer.ts @@ -1,12 +1,10 @@ import { ColumnUpdateNode, OperationNodeTransformer, - OperatorNode, PrimitiveValueListNode, ValueListNode, ValueNode, ValuesNode, - OperationNode } from 'kysely' import { Caster, @@ -63,7 +61,7 @@ export class SerializeParametersTransformer extends OperationNodeTransformer { return listNodeItem } - const { value, ...item } = listNodeItem as ValueNode + const { value } = listNodeItem as ValueNode const serializedValue = this.#serializer(value) @@ -91,7 +89,7 @@ export class SerializeParametersTransformer extends OperationNodeTransformer { return super.transformColumnUpdate(node) } - const { value, ...item } = valueNode as ValueNode + const { value } = valueNode as ValueNode const serializedValue = this.#serializer(value) diff --git a/packages/backend/src/OpenApi/swaggerSpec.ts b/packages/backend/src/OpenApi/swaggerSpec.ts index b4572de17..4b9cfd8aa 100644 --- a/packages/backend/src/OpenApi/swaggerSpec.ts +++ b/packages/backend/src/OpenApi/swaggerSpec.ts @@ -1,8 +1,11 @@ import swaggerJsdoc from 'swagger-jsdoc'; try{ + // intentionally require()'d (not the static `import` below) so a missing build + // of the shared package throws here, where we can give a friendlier error message + // eslint-disable-next-line @typescript-eslint/no-require-imports require('@equal-vote/star-vote-shared/schema.json') -}catch(e){ +}catch(_e){ throw "Could not find shared module. Did you build it?\n Try: npm run build -w @equal-vote/star-vote-shared" } diff --git a/packages/backend/src/Routes/registerEvents.ts b/packages/backend/src/Routes/registerEvents.ts index 73e898c73..4e35dab14 100644 --- a/packages/backend/src/Routes/registerEvents.ts +++ b/packages/backend/src/Routes/registerEvents.ts @@ -1,17 +1,19 @@ import ServiceLocator from "../ServiceLocator"; import Logger from "../Services/Logging/Logger"; - -const { handleCastVoteEvent } = require('../Controllers/Ballot/castVoteController'); -const { handleSendInviteEvent } = require('../Controllers/Election/sendInvitesController'); -const { handleSendEmailEvent } =require('../Controllers/Election/sendEmailController'); +import { EventHandler } from "../Services/EventQueue/IEventQueue"; +import { handleCastVoteEvent } from '../Controllers/Ballot/castVoteController'; +import { handleSendInviteEvent } from '../Controllers/Election/sendInvitesController'; +import { handleSendEmailEvent } from '../Controllers/Election/sendEmailController'; export default async function registerEvents() { const ctx = Logger.createContext("app init"); Logger.debug(ctx, "registering events"); const eventQueue = await ServiceLocator.eventQueue(); - eventQueue.subscribe("castVoteEvent", handleCastVoteEvent); - eventQueue.subscribe("sendInviteEvent", handleSendInviteEvent); - eventQueue.subscribe("sendEmailEvent", handleSendEmailEvent); + // Each handler's `data` is typed to its specific event shape (CastVoteEvent, etc.), narrower + // than EventHandler's generic `data: object` — cast rather than widen the shared subscribe() signature. + eventQueue.subscribe("castVoteEvent", handleCastVoteEvent as EventHandler); + eventQueue.subscribe("sendInviteEvent", handleSendInviteEvent as EventHandler); + eventQueue.subscribe("sendEmailEvent", handleSendEmailEvent as EventHandler); Logger.debug(ctx, "registering events complete"); } \ No newline at end of file diff --git a/packages/backend/src/ServiceLocator.ts b/packages/backend/src/ServiceLocator.ts index e86534c18..85a448495 100644 --- a/packages/backend/src/ServiceLocator.ts +++ b/packages/backend/src/ServiceLocator.ts @@ -16,10 +16,9 @@ import { Kysely, PostgresDialect } from 'kysely' import Cursor from 'pg-cursor'; import { Database } from "./Models/Database"; import { SerializeParametersPlugin } from "./Models/serialize-parameters/serialize-parameters-plugin"; +import { Pool } from 'pg'; -const { Pool } = require('pg'); - -var _postgresClient: any; +var _postgresClient: Pool; var _DB: Kysely var _appInitContext = Logger.createContext("appInit"); var _ballotsDb: IBallotStore; @@ -34,7 +33,7 @@ var _accountService: AccountService; var _globalData: GlobalData; -function postgres(): any { +function postgres(): Pool { if (_postgresClient == null) { var connectionConfig = pgConnectionObject(); // We can't log this since it has sensitive information @@ -71,7 +70,7 @@ function database(): Kysely { return _DB } -function pgConnectionObject(): any { +function pgConnectionObject(): { connectionString: string; ssl: { rejectUnauthorized: boolean } | false } { var connectionStr = pgConnectionString(); var devDB = process.env.DEV_DATABASE; if (devDB === 'TRUE') { @@ -157,6 +156,8 @@ function blobService(): BlobService { _blobService = new BlobService(); } else { Logger.info({}, 'AZURE_STORAGE_CONNECTION_STRING is not set. Using mock BlobService (image uploads will be no-ops).'); + // require lazily so production doesn't need to load the mock module + // eslint-disable-next-line @typescript-eslint/no-require-imports const MockBlobService = require("./Services/Blob/__mocks__/BlobService").default; _blobService = new MockBlobService(); } diff --git a/packages/backend/src/Services/Account/AccountService.ts b/packages/backend/src/Services/Account/AccountService.ts index 29261f28e..fcff2e4cd 100644 --- a/packages/backend/src/Services/Account/AccountService.ts +++ b/packages/backend/src/Services/Account/AccountService.ts @@ -3,6 +3,7 @@ import axios from 'axios'; import qs from 'qs'; import 'dotenv/config'; import { InternalServerError } from "@curveball/http-errors"; +import { Request } from 'express'; import { IRequest } from '../../IRequest'; import AccountServiceUtils from './AccountServiceUtils'; @@ -49,8 +50,8 @@ export default class AccountService { } } - getToken = async (req: any) => { - var params: any = { + getToken = async (req: Request) => { + var params: Record = { grant_type: req.query.grant_type, client_id: this.authConfig.clientId, redirect_uri: req.query.redirect_uri, @@ -86,8 +87,8 @@ export default class AccountService { ) Logger.debug(req, "success!"); return response.data - } catch (err: any) { - Logger.error(req, 'Error while requesting a token', err.response.data); + } catch (err: unknown) { + Logger.error(req, 'Error while requesting a token', axios.isAxiosError(err) ? err.response?.data : err); throw new InternalServerError("Error requesting token"); }; } diff --git a/packages/backend/src/Services/Account/AccountServiceUtils.ts b/packages/backend/src/Services/Account/AccountServiceUtils.ts index 1f56bd79d..d9881766a 100644 --- a/packages/backend/src/Services/Account/AccountServiceUtils.ts +++ b/packages/backend/src/Services/Account/AccountServiceUtils.ts @@ -1,8 +1,9 @@ import { IRequest } from "../../IRequest"; import Logger from "../Logging/Logger"; +import { getErrorMessage } from '../../errorUtils'; import { Unauthorized } from "@curveball/http-errors"; -const jwt = require("jsonwebtoken"); +import jwt from "jsonwebtoken"; export default class AccountServiceUtils { static extractUserFromRequest = ( @@ -19,8 +20,8 @@ export default class AccountServiceUtils { try { return jwt.verify(token, key, { algorithms }); - } catch (e: any) { - Logger.warn(req, "JWT Verify Error: ", e.message); + } catch (e: unknown) { + Logger.warn(req, "JWT Verify Error: ", getErrorMessage(e)); throw new Unauthorized(); } }; diff --git a/packages/backend/src/Services/Account/__mocks__/AccountService.ts b/packages/backend/src/Services/Account/__mocks__/AccountService.ts index df750d12a..a919319be 100644 --- a/packages/backend/src/Services/Account/__mocks__/AccountService.ts +++ b/packages/backend/src/Services/Account/__mocks__/AccountService.ts @@ -1,8 +1,8 @@ import { IRequest } from '../../../IRequest'; import Logger from "../../Logging/Logger"; import AccountServiceUtils from "../AccountServiceUtils"; - -var jwt = require('jsonwebtoken'); +import jwt from 'jsonwebtoken'; +import { Request } from 'express'; export default class AccountService { @@ -13,7 +13,7 @@ export default class AccountService { constructor() { } - getToken = async (req: any) => { + getToken = async (_req: Request) => { return {} } diff --git a/packages/backend/src/Services/Blob/BlobService.ts b/packages/backend/src/Services/Blob/BlobService.ts index dc7278d8a..cf35f1a0d 100644 --- a/packages/backend/src/Services/Blob/BlobService.ts +++ b/packages/backend/src/Services/Blob/BlobService.ts @@ -1,4 +1,4 @@ -let blobServiceClient: any = null; +import type { TransferProgressEvent } from '@azure/core-rest-pipeline'; export default class BlobService { client; @@ -9,6 +9,7 @@ export default class BlobService { throw new Error('AZURE_STORAGE_CONNECTION_STRING is not set. Set it in your environment or use the mock BlobService in tests.'); } // require lazily to avoid loading the azure sdk during build/generate steps + // eslint-disable-next-line @typescript-eslint/no-require-imports const { BlobServiceClient } = require('@azure/storage-blob'); this.client = BlobServiceClient.fromConnectionString(connectionString); } @@ -18,7 +19,7 @@ export default class BlobService { blobName: string, buffer: Buffer, contentType?: string, - onProgress?: (progress: any) => void, + onProgress?: (progress: TransferProgressEvent) => void, ): Promise => { if(!this.client) throw new Error("Couldn't upload to blob, client wasn't initialized since AZURE_STORAGE_CONNECTION_STRING wasn't properly set") diff --git a/packages/backend/src/Services/Blob/__mocks__/BlobService.ts b/packages/backend/src/Services/Blob/__mocks__/BlobService.ts index 1f808015b..bf92e6f12 100644 --- a/packages/backend/src/Services/Blob/__mocks__/BlobService.ts +++ b/packages/backend/src/Services/Blob/__mocks__/BlobService.ts @@ -1,3 +1,5 @@ +import type { TransferProgressEvent } from '@azure/core-rest-pipeline'; + export default class BlobService { public uploaded: { containerName: string; blobName: string; contentType?: string; buffer: Buffer }[]; @@ -10,7 +12,7 @@ export default class BlobService { blobName: string, buffer: Buffer, contentType?: string, - onProgress?: (progress: any) => void, + _onProgress?: (progress: TransferProgressEvent) => void, ) => { this.uploaded.push({ containerName, blobName, contentType, buffer }); return `https://mock.blob/${containerName}/${blobName}`; diff --git a/packages/backend/src/Services/Email/EmailService.ts b/packages/backend/src/Services/Email/EmailService.ts index c26d41c84..e41306dc5 100644 --- a/packages/backend/src/Services/Email/EmailService.ts +++ b/packages/backend/src/Services/Email/EmailService.ts @@ -1,12 +1,18 @@ import { Imsg } from "./IEmail" +import sgMail from '@sendgrid/mail' +import 'dotenv/config' + export default class EmailService { - sgMail; + // Untyped: @sendgrid/mail's declared return type for send() doesn't match its + // actual runtime shape when passed an array of messages (see callers indexing + // into the response as an array of responses), so callers rely on this being loose. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + sgMail: any; constructor() { - this.sgMail = require('@sendgrid/mail') - require('dotenv').config() - this.sgMail.setApiKey(process.env.SENDGRID_API_KEY) + this.sgMail = sgMail + this.sgMail.setApiKey(process.env.SENDGRID_API_KEY ?? '') } sendEmails = async (msg: Imsg[]) => { diff --git a/packages/backend/src/Services/EventQueue/MockEventQueue.ts b/packages/backend/src/Services/EventQueue/MockEventQueue.ts index f0a6e7314..a2313e04c 100644 --- a/packages/backend/src/Services/EventQueue/MockEventQueue.ts +++ b/packages/backend/src/Services/EventQueue/MockEventQueue.ts @@ -1,6 +1,5 @@ import { randomUUID } from "crypto"; -import { ILoggingContext } from "../Logging/ILogger"; -import { EventHandler, IEventQueue, JobInsert } from "./IEventQueue"; +import { EventHandler, IEventQueue } from "./IEventQueue"; import { QueueName } from "./QueueName"; type Job = { @@ -73,7 +72,7 @@ export class MockEventQueue implements IEventQueue { try { console.info("MEQ: Processing job: " + JSON.stringify(j)); await this.doJob(j); - } catch (e:any) { + } catch (_e: unknown) { console.info("MEQ: Exception handling job: " + JSON.stringify(j)); } this._working = false; diff --git a/packages/backend/src/Services/EventQueue/PGBossEventQueue.ts b/packages/backend/src/Services/EventQueue/PGBossEventQueue.ts index 48e2a0d4c..7d0019623 100644 --- a/packages/backend/src/Services/EventQueue/PGBossEventQueue.ts +++ b/packages/backend/src/Services/EventQueue/PGBossEventQueue.ts @@ -2,20 +2,23 @@ import { ILoggingContext } from "../Logging/ILogger"; import Logger from "../Logging/Logger"; import { EventHandler, IEventQueue, JobInsert } from "./IEventQueue"; import { QueueName } from "./QueueName"; - - +import PgBoss from 'pg-boss'; export default class PGBossEventQueue implements IEventQueue { + // Untyped: pg-boss's installed API (send()/insert()/countStates()) has drifted from what + // this class assumes (e.g. debugInfo()'s countStates() call doesn't exist on the current + // types), pre-dating this pass. Typing it properly means reconciling that drift, which is + // out of scope here. + // eslint-disable-next-line @typescript-eslint/no-explicit-any _boss: any; constructor() { } public async init(pgConnection: object, ctx: ILoggingContext): Promise { - const PgBoss = require('pg-boss'); this._boss = new PgBoss(pgConnection); - this._boss.on('error', (error: any) => Logger.error(ctx, error)); + this._boss.on('error', (error: unknown) => Logger.error(ctx, error)); await this._boss.start(); return this; diff --git a/packages/backend/src/Services/Logging/ILogger.ts b/packages/backend/src/Services/Logging/ILogger.ts index 316acafe7..ded9ccacb 100644 --- a/packages/backend/src/Services/Logging/ILogger.ts +++ b/packages/backend/src/Services/Logging/ILogger.ts @@ -8,8 +8,8 @@ export interface ICustomContext { }; export interface ILogger { - debug(context?:ILoggingContext, message?: any, ...optionalParams: any[]):void; - info(context?:ILoggingContext, message?: any, ...optionalParams: any[]):void; - warn(context?:ILoggingContext, message?: any, ...optionalParams: any[]):void; - error(context?:ILoggingContext, message?: any, ...optionalParams: any[]):void; + debug(context?:ILoggingContext, message?: unknown, ...optionalParams: unknown[]):void; + info(context?:ILoggingContext, message?: unknown, ...optionalParams: unknown[]):void; + warn(context?:ILoggingContext, message?: unknown, ...optionalParams: unknown[]):void; + error(context?:ILoggingContext, message?: unknown, ...optionalParams: unknown[]):void; } \ No newline at end of file diff --git a/packages/backend/src/Services/Logging/Logger.ts b/packages/backend/src/Services/Logging/Logger.ts index e0e449cc3..f337559b5 100644 --- a/packages/backend/src/Services/Logging/Logger.ts +++ b/packages/backend/src/Services/Logging/Logger.ts @@ -3,19 +3,19 @@ import { LoggerImpl } from "./LoggerImpl"; var _loggerInstance: ILogger; -function debug(context?: ILoggingContext, message?: any, ...optionalParams: any[]): void { +function debug(context?: ILoggingContext, message?: unknown, ...optionalParams: unknown[]): void { logger().debug(context, message, ...optionalParams); } -function info(context?: ILoggingContext,message?: any, ...optionalParams: any[]): void { +function info(context?: ILoggingContext,message?: unknown, ...optionalParams: unknown[]): void { logger().info(context, message, ...optionalParams); } -function warn(context?: ILoggingContext,message?: any, ...optionalParams: any[]): void { +function warn(context?: ILoggingContext,message?: unknown, ...optionalParams: unknown[]): void { logger().warn(context, message, ...optionalParams); } -function error(context?: ILoggingContext, message?: any, ...optionalParams: any[]): void { +function error(context?: ILoggingContext, message?: unknown, ...optionalParams: unknown[]): void { logger().error(context, message, ...optionalParams); } @@ -23,7 +23,7 @@ function error(context?: ILoggingContext, message?: any, ...optionalParams: any[ * Use to log about a state change (ie write to a DB) * Shortcut for Logger.info with a prefix **/ -function state(context?: ILoggingContext, message?: any, ...optionalParams: any[]): void { +function state(context?: ILoggingContext, message?: unknown, ...optionalParams: unknown[]): void { logger().info(context, "STATE: " + message, ...optionalParams); } diff --git a/packages/backend/src/Services/Logging/LoggerImpl.ts b/packages/backend/src/Services/Logging/LoggerImpl.ts index 37d1ee4d2..bbc5dc3ca 100644 --- a/packages/backend/src/Services/Logging/LoggerImpl.ts +++ b/packages/backend/src/Services/Logging/LoggerImpl.ts @@ -1,4 +1,3 @@ -import { randomUUID } from "crypto"; import { ILoggingContext } from "./ILogger"; @@ -20,27 +19,27 @@ export class LoggerImpl { constructor() { } - debug(context?:ILoggingContext, message?: any, ...optionalParams: any[]):void{ + debug(context?:ILoggingContext, message?: unknown, ...optionalParams: unknown[]):void{ if (!shouldLog('debug')) return; this.log(context, "", message, ...optionalParams); } - info(context?:ILoggingContext, message?: any, ...optionalParams: any[]):void{ + info(context?:ILoggingContext, message?: unknown, ...optionalParams: unknown[]):void{ if (!shouldLog('info')) return; this.log(context, "", message, ...optionalParams); } - warn(context?:ILoggingContext, message?: any, ...optionalParams: any[]):void{ + warn(context?:ILoggingContext, message?: unknown, ...optionalParams: unknown[]):void{ if (!shouldLog('warn')) return; this.log(context, "WARN ", message, ...optionalParams); } - error(context?:ILoggingContext, message?: any, ...optionalParams: any[]):void{ + error(context?:ILoggingContext, message?: unknown, ...optionalParams: unknown[]):void{ if (!shouldLog('error')) return; this.log(context, "ERROR", message, ...optionalParams); } - log(context?:ILoggingContext, levelStr?:string, message?: any, ...optionalParams: any[]):void { + log(context?:ILoggingContext, levelStr?:string, message?: unknown, ...optionalParams: unknown[]):void { var msg = ""; var lvlStr = ""; var ctxStr = ""; diff --git a/packages/backend/src/Services/Logging/LoggerMiddleware.ts b/packages/backend/src/Services/Logging/LoggerMiddleware.ts index d0119d13b..d4ed47359 100644 --- a/packages/backend/src/Services/Logging/LoggerMiddleware.ts +++ b/packages/backend/src/Services/Logging/LoggerMiddleware.ts @@ -1,8 +1,9 @@ import { IRequest } from "../../IRequest"; import Logger from "./Logger"; import { logSafeHash } from "./logSafeHash"; +import { Response, NextFunction } from 'express'; -export function loggerMiddleware(req: IRequest, res: any, next: any): void { +export function loggerMiddleware(req: IRequest, res: Response, next: NextFunction): void { Logger.info({ contextId: req.contextId, logPrefix: '\n' }, `\nREQUEST: ${req.method} ${req.url} @ ${new Date(Date.now()).toISOString()} ip:${logSafeHash(req.ip)}`); res.on('finish', () => { diff --git a/packages/backend/src/Services/Logging/TestLoggerImpl.ts b/packages/backend/src/Services/Logging/TestLoggerImpl.ts index d6631dc46..4fb0351c3 100644 --- a/packages/backend/src/Services/Logging/TestLoggerImpl.ts +++ b/packages/backend/src/Services/Logging/TestLoggerImpl.ts @@ -1,4 +1,3 @@ -import { randomUUID } from "crypto"; import { ILoggingContext } from "./ILogger"; import Logger from "./Logger"; @@ -10,19 +9,19 @@ export class TestLoggerImpl { constructor() { } - debug(context?:ILoggingContext, message?: any, ...optionalParams: any[]):void{ + debug(context?:ILoggingContext, message?: unknown, ...optionalParams: unknown[]):void{ this.log(context, "", message, ...optionalParams); } - info(context?:ILoggingContext, message?: any, ...optionalParams: any[]):void{ + info(context?:ILoggingContext, message?: unknown, ...optionalParams: unknown[]):void{ this.log(context, "", message, ...optionalParams); } - warn(context?:ILoggingContext, message?: any, ...optionalParams: any[]):void{ + warn(context?:ILoggingContext, message?: unknown, ...optionalParams: unknown[]):void{ this.log(context, "WARN ", message, ...optionalParams); } - error(context?:ILoggingContext, message?: any, ...optionalParams: any[]):void{ + error(context?:ILoggingContext, message?: unknown, ...optionalParams: unknown[]):void{ //TODO - put more structure to the data shared in request and spit it all out here this.log(context, "ERROR", message, ...optionalParams); } @@ -42,7 +41,7 @@ export class TestLoggerImpl { this.logs=[]; } - log(context?:ILoggingContext, levelStr?:string, message?: any, ...optionalParams: any[]):void { + log(context?:ILoggingContext, levelStr?:string, message?: unknown, ...optionalParams: unknown[]):void { var msg = ""; var lvlStr = ""; var ctxStr = ""; diff --git a/packages/backend/src/Tabulators/AllocatedScore.ts b/packages/backend/src/Tabulators/AllocatedScore.ts index 1cf31668e..bad549dee 100644 --- a/packages/backend/src/Tabulators/AllocatedScore.ts +++ b/packages/backend/src/Tabulators/AllocatedScore.ts @@ -1,5 +1,9 @@ -import { candidate, allocatedScoreResults, allocatedScoreSummaryData, rawVote, allocatedScoreCandidate, vote } from "@equal-vote/star-vote-shared/domain_model/ITabulators"; +import { candidate, allocatedScoreResults, allocatedScoreSummaryData, rawVote, allocatedScoreCandidate } from "@equal-vote/star-vote-shared/domain_model/ITabulators"; +// require()'d rather than imported: fraction.js's type declarations don't match how this +// file uses Fraction (mixing static/instance members), and require()'s implicit `any` +// papers over that mismatch. Reworking the typing is out of scope for a lint pass. +// eslint-disable-next-line @typescript-eslint/no-require-imports const Fraction = require('fraction.js'); import { getSummaryData, makeAbstentionTest, makeBoundsTest, sortCandidates } from "./Util"; import { ElectionSettings } from "@equal-vote/star-vote-shared/domain_model/ElectionSettings"; @@ -14,7 +18,7 @@ type ballotFrac = typeof Fraction[] const MAX_SCORE = 5; -export function AllocatedScore(candidates: candidate[], votes: rawVote[], nWinners = 3, electionSettings?:ElectionSettings) { +export function AllocatedScore(candidates: candidate[], votes: rawVote[], nWinners = 3, _electionSettings?:ElectionSettings) { const {summaryData: initialSummaryData, tallyVotes} = getSummaryData>( candidates.map(c => ({...c, score: 0})), @@ -98,7 +102,7 @@ export function AllocatedScore(candidates: candidate[], votes: rawVote[], nWinne } results.tied.push(...maxAndTies.ties); // Set all scores for winner to zero - scoresNorm.forEach((ballot, b) => { + scoresNorm.forEach((ballot, _b) => { ballot[w] = new Fraction(0) }) remainingCandidates = remainingCandidates.filter(c => c != summaryData.candidates[w]) @@ -130,7 +134,7 @@ export function AllocatedScore(candidates: candidate[], votes: rawVote[], nWinne summaryData.splitPoints.push(split_point.valueOf()); let spent_above = new Fraction(0); - cand_df.forEach((c, i) => { + cand_df.forEach((c, _i) => { if (c.weighted_score.compare(split_point) > 0) { spent_above = spent_above.add(c.ballot_weight); } @@ -249,7 +253,7 @@ function updateBallotWeights( function findWeightOnSplit(cand_df: winner_scores[], split_point: typeof Fraction) { let weight_on_split = new Fraction(0); - cand_df.forEach((c, i) => { + cand_df.forEach((c, _i) => { if (c.weighted_score.equals(split_point)) { weight_on_split = weight_on_split.add(c.ballot_weight); } diff --git a/packages/backend/src/Tabulators/Approval.ts b/packages/backend/src/Tabulators/Approval.ts index 76917e9ce..ea45d954a 100644 --- a/packages/backend/src/Tabulators/Approval.ts +++ b/packages/backend/src/Tabulators/Approval.ts @@ -3,7 +3,7 @@ import { approvalResults, approvalCandidate, approvalSummaryData, candidate, raw import { commaListFormatter, makeBoundsTest, makeAbstentionTest, runBlocTabulator, getSummaryData } from "./Util"; import { ElectionSettings } from "@equal-vote/star-vote-shared/domain_model/ElectionSettings"; -export function Approval(candidates: candidate[], votes: rawVote[], nWinners = 1, electionSettings?:ElectionSettings) { +export function Approval(candidates: candidate[], votes: rawVote[], nWinners = 1, _electionSettings?:ElectionSettings) { const {summaryData} = getSummaryData( candidates.map(c => ({...c, score: 0})), votes, @@ -31,7 +31,7 @@ export function Approval(candidates: candidate[], votes: rawVote[], nWinners = 1 ) } -const singleWinnerApproval = (remainingCandidates: approvalCandidate[], summaryData: approvalSummaryData): approvalRoundResults => { +const singleWinnerApproval = (remainingCandidates: approvalCandidate[], _summaryData: approvalSummaryData): approvalRoundResults => { let winner = remainingCandidates[0]; let tiedCandidates = remainingCandidates.filter(c => c.score == winner.score); diff --git a/packages/backend/src/Tabulators/IRV.ts b/packages/backend/src/Tabulators/IRV.ts index cbaa3ab69..c17ee5534 100644 --- a/packages/backend/src/Tabulators/IRV.ts +++ b/packages/backend/src/Tabulators/IRV.ts @@ -3,6 +3,10 @@ import { candidate, irvCandidate, irvResults, irvRoundResults, irvSummaryData, k import { getSummaryData, makeAbstentionTest, makeBoundsTest, sortCandidates } from "./Util"; import { ElectionSettings } from "@equal-vote/star-vote-shared/domain_model/ElectionSettings"; +// require()'d rather than imported: fraction.js's type declarations don't match how this +// file uses Fraction (mixing static/instance members), and require()'s implicit `any` +// papers over that mismatch. Reworking the typing is out of scope for a lint pass. +// eslint-disable-next-line @typescript-eslint/no-require-imports const Fraction = require('fraction.js'); const DEBUG = false; diff --git a/packages/backend/src/Tabulators/NoBallots.test.ts b/packages/backend/src/Tabulators/NoBallots.test.ts index 9aa02a332..6f7904f0e 100644 --- a/packages/backend/src/Tabulators/NoBallots.test.ts +++ b/packages/backend/src/Tabulators/NoBallots.test.ts @@ -13,7 +13,7 @@ describe("Tabulating a race nobody has voted in", () => { Object.keys(VotingMethods).forEach(method => { test(`${method} tabulates with no ballots`, () => { const [c, v] = mapMethodInputs(candidates, []) - const results = (VotingMethods as any)[method](c, v, 5, {}) + const results = VotingMethods[method as keyof typeof VotingMethods](c, v, 5) expect(results).toBeDefined() expect(results.summaryData.nTallyVotes).toBe(0) diff --git a/packages/backend/src/Tabulators/Plurality.ts b/packages/backend/src/Tabulators/Plurality.ts index 985ffebc7..414c79b09 100644 --- a/packages/backend/src/Tabulators/Plurality.ts +++ b/packages/backend/src/Tabulators/Plurality.ts @@ -1,9 +1,9 @@ -import { candidate, pluralityCandidate, pluralityResults, pluralitySummaryData, plurlaityRoundResults, rawVote, roundResults } from "@equal-vote/star-vote-shared/domain_model/ITabulators"; +import { candidate, pluralityCandidate, pluralityResults, pluralitySummaryData, plurlaityRoundResults, rawVote } from "@equal-vote/star-vote-shared/domain_model/ITabulators"; import { commaListFormatter, makeBoundsTest, makeAbstentionTest, runBlocTabulator, getSummaryData } from "./Util"; import { ElectionSettings } from "@equal-vote/star-vote-shared/domain_model/ElectionSettings"; -export function Plurality(candidates: candidate[], votes: rawVote[], nWinners = 1, electionSettings?:ElectionSettings) { +export function Plurality(candidates: candidate[], votes: rawVote[], nWinners = 1, _electionSettings?:ElectionSettings) { const {summaryData} = getSummaryData( // ordinal would be more correct, but for computing totalScores plurlaity uses cardinal rules candidates.map(c => ({...c, score: 0})), @@ -33,7 +33,7 @@ export function Plurality(candidates: candidate[], votes: rawVote[], nWinners = ); } -const singleWinnerPlurality = (remainingCandidates: pluralityCandidate[], summaryData: pluralitySummaryData): plurlaityRoundResults => { +const singleWinnerPlurality = (remainingCandidates: pluralityCandidate[], _summaryData: pluralitySummaryData): plurlaityRoundResults => { let winner = remainingCandidates[0]; let tiedCandidates = remainingCandidates.filter(c => c.score == winner.score); diff --git a/packages/backend/src/Tabulators/RankedRobin.ts b/packages/backend/src/Tabulators/RankedRobin.ts index d7259e1ef..d678dbeb7 100644 --- a/packages/backend/src/Tabulators/RankedRobin.ts +++ b/packages/backend/src/Tabulators/RankedRobin.ts @@ -30,7 +30,7 @@ export function RankedRobin(candidates: candidate[], votes: rawVote[], nWinners ); } -const singleWinnerRankedRobin = (remainingCandidates: rankedRobinCandidate[], summaryData: rankedRobinSummaryData): rankedRobinRoundResults => { +const singleWinnerRankedRobin = (remainingCandidates: rankedRobinCandidate[], _summaryData: rankedRobinSummaryData): rankedRobinRoundResults => { // Initialize output results data structure const roundResults: rankedRobinRoundResults = { winners: [], diff --git a/packages/backend/src/Tabulators/Star.ts b/packages/backend/src/Tabulators/Star.ts index 6c3a5e16e..7a0753989 100644 --- a/packages/backend/src/Tabulators/Star.ts +++ b/packages/backend/src/Tabulators/Star.ts @@ -2,8 +2,8 @@ import { candidate, starResults, roundResults, starSummaryData, starCandidate, r import { getSummaryData, makeAbstentionTest, makeBoundsTest, runBlocTabulator, sortCandidates } from "./Util"; import { ElectionSettings } from "@equal-vote/star-vote-shared/domain_model/ElectionSettings"; -export function Star(candidates: candidate[], votes: rawVote[], nWinners = 1, electionSettings?:ElectionSettings) { - const {tallyVotes, summaryData} = getSummaryData( +export function Star(candidates: candidate[], votes: rawVote[], nWinners = 1, _electionSettings?:ElectionSettings) { + const {tallyVotes: _tallyVotes, summaryData} = getSummaryData( candidates.map(c => ({...c, score: 0, fiveStarCount: 0})), votes, 'cardinal', diff --git a/packages/backend/src/Tabulators/Util.ts b/packages/backend/src/Tabulators/Util.ts index 8553a93a7..029436703 100644 --- a/packages/backend/src/Tabulators/Util.ts +++ b/packages/backend/src/Tabulators/Util.ts @@ -1,5 +1,9 @@ import { candidate, genericResults, genericSummaryData, rawVote, roundResults, vote } from "@equal-vote/star-vote-shared/domain_model/ITabulators"; +// require()'d rather than imported: fraction.js's type declarations don't match how this +// file uses Fraction (mixing static/instance members), and require()'s implicit `any` +// papers over that mismatch. Reworking the typing is out of scope for a lint pass. +// eslint-disable-next-line @typescript-eslint/no-require-imports const Fraction = require('fraction.js'); declare namespace Intl { class ListFormat { @@ -10,82 +14,6 @@ declare namespace Intl { // converts list of strings to string with correct grammar ([a,b,c] => 'a, b, and c') export const commaListFormatter = new Intl.ListFormat('en', { style: 'long', type: 'conjunction' }); -// Format a Timestamp value into a compact string for display; -function formatTimestamp(value : string) { - const d = new Date(Date.parse(value)); - const month = d.getMonth() + 1; - const date = d.getDate(); - const year = d.getFullYear(); - const currentYear = new Date().getFullYear(); - const hour = d.getHours(); - const minute = d.getMinutes(); - - const fullDate = - year === currentYear - ? `${month}/${date}` - : year >= 2000 && year < 2100 - ? `${month}/${date}/${year - 2000}` - : `${month}/${date}/${year}`; - - const timeStamp = `${fullDate} ${hour}:${minute}`; - return timeStamp; -} - - - -const isScore = (value : any) => - !isNaN(value) && (value === null || (value > -10 && value < 10)); - -const transformScore = (value : number) => { - // minScore and maxScore were undefined when moving the file to typescript, so I'm hard coding them for now - const minScore = 0; - const maxScore = 5; - value ? Math.min(maxScore, Math.max(minScore, value)) : 0; -} - -// Functions to parse Timestamps -const isTimestamp = (value : any) => !isNaN(Date.parse(value)); -const transformTimestamp = (value : any) => formatTimestamp(value); - -// Functions to parse everything else -const isAny = (value : any) => true; -const transformAny = (value : any) => (value ? value.toString().trim() : ""); - -// Column types to recognize in Cast Vote Records passed as CSV data -const columnTypes = [ - { test: isScore, transform: transformScore }, - { test: isTimestamp, transform: transformTimestamp }, - // Last row MUST accept anything! - { test: isAny, transform: transformAny } -]; - - -function getTransforms(header : any, data : string[][]) { - const transforms : any[] = []; - const rowCount = Math.min(data.length, 3); - header.forEach((title : string, n : number) => { - var transformIndex = 0; - if (title === "Timestamp") { - transformIndex = 1; - } else { - for (let i = 0; i < rowCount; i++) { - const value = data[i][n]; - const index = columnTypes.findIndex((element) => element.test(value)); - if (index > transformIndex) { - transformIndex = index; - } - if (transformIndex >= columnTypes.length) { - break; - } - } - } - // We don't have to check for out-of-bound index because - // the last row in columnTypes accepts anything - transforms.push(columnTypes[transformIndex].transform); - }); - return transforms; -} - export const makeBoundsTest = (minValue:number, maxValue:number) => { return [ 'nOutOfBoundsVotes', @@ -109,7 +37,7 @@ const filterInitialVotes = (rawVotes: rawVote[], candidateIds: string[], tests: let tallyVotes: vote[] = []; let summaryStats: {[key: string]: number} = {}; - tests.forEach(([statName, statTest]) => { + tests.forEach(([statName, _statTest]) => { summaryStats[statName] = 0; }) summaryStats['nTallyVotes'] = 0; diff --git a/packages/backend/src/Tabulators/VotingMethodSelecter.ts b/packages/backend/src/Tabulators/VotingMethodSelecter.ts index 1ad38a17a..923ff2180 100644 --- a/packages/backend/src/Tabulators/VotingMethodSelecter.ts +++ b/packages/backend/src/Tabulators/VotingMethodSelecter.ts @@ -5,7 +5,7 @@ import { IRV, STV } from "./IRV"; import { RankedRobin } from "./RankedRobin"; import { AllocatedScore } from "./AllocatedScore"; import { ElectionSettings} from "@equal-vote/star-vote-shared/domain_model/ElectionSettings"; -import { allocatedScoreCandidate, allocatedScoreResults, allocatedScoreSummaryData, approvalCandidate, approvalResults, approvalSummaryData, candidate, genericResults, genericSummaryData, irvCandidate, irvResults, irvSummaryData, pluralityCandidate, pluralityResults, pluralitySummaryData, rankedRobinCandidate, rankedRobinResults, rankedRobinRoundResults, rankedRobinSummaryData, rawVote, starCandidate, starResults, starSummaryData, vote } from "@equal-vote/star-vote-shared/domain_model/ITabulators"; +import { allocatedScoreCandidate, allocatedScoreResults, allocatedScoreSummaryData, approvalCandidate, approvalResults, approvalSummaryData, candidate, genericResults, genericSummaryData, irvCandidate, irvResults, irvSummaryData, pluralityCandidate, pluralityResults, pluralitySummaryData, rankedRobinCandidate, rankedRobinResults, rankedRobinSummaryData, rawVote, starCandidate, starResults, starSummaryData } from "@equal-vote/star-vote-shared/domain_model/ITabulators"; type TabulatorFunction< CandidateType extends candidate, diff --git a/packages/backend/src/Tabulators/testApproval.js b/packages/backend/src/Tabulators/testApproval.js index 927e42fbb..c3cae0b9c 100644 --- a/packages/backend/src/Tabulators/testApproval.js +++ b/packages/backend/src/Tabulators/testApproval.js @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-require-imports -- standalone CJS script, run directly with `node` */ const ApprovalResults = require('./ApprovalResults') const candidates = ['Alice','Bob','Carol','Dave'] @@ -11,4 +12,4 @@ const votes = [ ] -const results = ApprovalResults(candidates,votes) \ No newline at end of file +ApprovalResults(candidates,votes) \ No newline at end of file diff --git a/packages/backend/src/Tabulators/testPlurality.js b/packages/backend/src/Tabulators/testPlurality.js index 4c85827da..aa6509ee2 100644 --- a/packages/backend/src/Tabulators/testPlurality.js +++ b/packages/backend/src/Tabulators/testPlurality.js @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-require-imports -- standalone CJS script, run directly with `node` */ const PluralityResults = require('./PluralityResults') const candidates = ['Alice','Bob','Carol','Dave'] @@ -11,4 +12,4 @@ const votes = [ ] -const results = PluralityResults(candidates,votes) \ No newline at end of file +PluralityResults(candidates,votes) \ No newline at end of file diff --git a/packages/backend/src/Tabulators/tinyrand.ts b/packages/backend/src/Tabulators/tinyrand.ts index fd20cf193..f0e5e7068 100644 --- a/packages/backend/src/Tabulators/tinyrand.ts +++ b/packages/backend/src/Tabulators/tinyrand.ts @@ -2,7 +2,6 @@ const SUPPORTED_VERSIONS: readonly number[] = [0]; const DEFAULT_VERSION: number = 0; -const MASK32: number = 0xffffffff; // Note: In Python, NSTATES is set to 1 << BITS (with BITS === 32) so that // NSTATES is 2^32. (That is our “upper‐bound” check for shuffle().) diff --git a/packages/backend/src/Util.ts b/packages/backend/src/Util.ts index 1d857ae42..d85750463 100644 --- a/packages/backend/src/Util.ts +++ b/packages/backend/src/Util.ts @@ -18,13 +18,13 @@ export function orDefault(data: T | null, def:T):T { return data; } -export function responseErr(res:Response, req:Request, code:number, errMessage:string, extraData?:any){ +export function responseErr(res:Response, req:Request, code:number, errMessage:string, extraData?:Record): void { errMessage += reqIdSuffix(req); if (extraData == null){ extraData = {}; } extraData.error = errMessage; - return res.status(code).json(extraData); + res.status(code).json(extraData); } interface ImageKitLayer{ @@ -36,7 +36,7 @@ const formatImageKitURL = (layers: ImageKitLayer[]) : string => { ...layers.map( l => [ l['type'], - ...[Object.entries(l).filter(([k, v]) => k != 'type').map(([k, v]) => `${k}-${encodeURIComponent(v)}`)], + ...[Object.entries(l).filter(([k, _v]) => k != 'type').map(([k, v]) => `${k}-${encodeURIComponent(v)}`)], ((l['type'] as string).startsWith('l')? 'l-end' : '') ].join(',') ).join(':'), @@ -55,7 +55,7 @@ interface TagObject{ [key: string]: string } let ElectionsModel = ServiceLocator.electionsDb(); -export async function getMetaTags(req: any) : Promise { +export async function getMetaTags(req: Request) : Promise { let parts = req.url.split('/'); let election:Election|null; @@ -66,7 +66,7 @@ export async function getMetaTags(req: any) : Promise { const electionID = (parts[1] == 'Election' ? parts[2] : parts[1]) try{ election = await ElectionsModel.getElectionByID(electionID, req); - } catch (err:any) { + } catch (_err: unknown) { election = null; } } diff --git a/packages/backend/src/app.ts b/packages/backend/src/app.ts index 5bb4b92cb..b277e2dbd 100644 --- a/packages/backend/src/app.ts +++ b/packages/backend/src/app.ts @@ -1,5 +1,7 @@ -require('dotenv').config(); +import 'dotenv/config'; import express from 'express'; +import path from 'path'; +import fs from 'fs'; import {electionsRouter, ballotRouter, rollRouter} from './Routes'; // var debugRouter = require('./Routes/debug.routes') @@ -7,7 +9,7 @@ import cors from 'cors'; import compression from 'compression'; import cookieParser from 'cookie-parser'; import Logger from './Services/Logging/Logger'; -import {IRequest, iRequestMiddleware, reqIdSuffix} from './IRequest'; +import {iRequestMiddleware} from './IRequest'; import { loggerMiddleware } from './Services/Logging/LoggerMiddleware'; import { errorCatch } from './errorCatchMiddleware' import registerEvents from './Routes/registerEvents'; @@ -17,8 +19,8 @@ import swagger from './OpenApi/swagger.json'; import { getUserToken, getUser } from './Controllers/User'; import { sendGridWebhookController } from './Controllers/sendGridWebhookController'; -const asyncHandler = require('express-async-handler') -require('./socketHandler') +import asyncHandler from 'express-async-handler' +import './socketHandler' export default function makeApp() { const app = express(); @@ -26,7 +28,7 @@ export default function makeApp() { // CORS (Cross-origin resource sharing), allows for the backend to receive calls from the front end, even though they have different urls/origins // (at least that's my understanding) - const prodEndpoints : any = process.env.ALLOWED_URLS?.split(',') || 'https://bettervoting.com/'; + const prodEndpoints: string[] | string = process.env.ALLOWED_URLS?.split(',') || 'https://bettervoting.com/'; app.use(cors({ origin: prodEndpoints, credentials: true, // allow the backend to receive cookies from the frontend @@ -48,8 +50,7 @@ export default function makeApp() { app.use(cookieParser()) const frontendPath = '../../../../packages/frontend/build/'; - - const path = require('path'); + // SendGrid webhook must be registered before express.json() to preserve the raw body for signature verification app.post('/API/SendGridWebhook', express.raw({ type: 'application/json' }), sendGridWebhookController); @@ -65,13 +66,12 @@ export default function makeApp() { // NOTE: I've removed express.static because it doesn't allow me to inject meta tags // https://stackoverflow.com/questions/51120214/how-to-modify-static-file-content-with-express-static app.get('*', (req, res) => { - const fs = require('fs'); - fs.readFile(path.join(__dirname, frontendPath, req.url.split('?')[0]), 'utf8', (err:any, htmlData:string) => { + fs.readFile(path.join(__dirname, frontendPath, req.url.split('?')[0]), 'utf8', (err, _htmlData) => { if(err){ // if the request wants a webpage, then return index.html and inject meta tags // https://blog.logrocket.com/adding-dynamic-meta-tags-react-app-without-ssr/ - fs.readFile(path.join(__dirname, frontendPath, 'index.html'), 'utf8', async (err:any, htmlData:string) => { + fs.readFile(path.join(__dirname, frontendPath, 'index.html'), 'utf8', async (err, htmlData) => { if(err){ console.error('Error during file reading', err); return res.status(404).end(); diff --git a/packages/backend/src/auth/MockUserStore.ts b/packages/backend/src/auth/MockUserStore.ts index 8a9f880bd..4cd0efadf 100644 --- a/packages/backend/src/auth/MockUserStore.ts +++ b/packages/backend/src/auth/MockUserStore.ts @@ -1,7 +1,6 @@ import { Email } from "@equal-vote/star-vote-shared/domain_model/Email"; import { Uid } from "@equal-vote/star-vote-shared/domain_model/Uid"; import { UserModel } from "./data_model/UserModel"; -import { IUserStore } from "./IUserStore"; diff --git a/packages/backend/src/auth/test/TestMockUserStore.ts b/packages/backend/src/auth/test/TestMockUserStore.ts index 933256865..543a689ea 100644 --- a/packages/backend/src/auth/test/TestMockUserStore.ts +++ b/packages/backend/src/auth/test/TestMockUserStore.ts @@ -39,7 +39,7 @@ export async function testMockUserStore(): Promise { } -function assertSame(data1:any, data2:any, message:string) { +function assertSame(data1:unknown, data2:unknown, message:string) { if (data1 != data2){ throw(new Error(`${message}\n${JSON.stringify(data1)} != ${JSON.stringify(data2)}`)); } diff --git a/packages/backend/src/errorCatchMiddleware.ts b/packages/backend/src/errorCatchMiddleware.ts index d28db5f21..060919d4a 100644 --- a/packages/backend/src/errorCatchMiddleware.ts +++ b/packages/backend/src/errorCatchMiddleware.ts @@ -1,15 +1,20 @@ import Logger from "./Services/Logging/Logger" -import { reqIdSuffix } from "./IRequest" -export const errorCatch = async (err: any, req: any, res: any, next: any) => { - Logger.error(req, err.message); - var status = 500; - if (err.httpStatus) { +import { IRequest, reqIdSuffix } from "./IRequest" +import { Response, NextFunction } from 'express'; +import { HttpErrorBase } from "@curveball/http-errors"; +import { getErrorMessage } from './errorUtils'; + +export const errorCatch = async (err: unknown, req: IRequest, res: Response, _next: NextFunction) => { + const message = getErrorMessage(err); + Logger.error(req, message); + let status = 500; + let msg = "Error"; + if (err instanceof HttpErrorBase) { status = err.httpStatus; - } - var msg = "Error"; - if (err.detail) { - msg = err.detail; + if (err.detail) { + msg = err.detail; + } } msg += reqIdSuffix(req); - return res.status(status).json({ error: msg }); + res.status(status).json({ error: msg }); } diff --git a/packages/backend/src/errorUtils.ts b/packages/backend/src/errorUtils.ts new file mode 100644 index 000000000..4a0cbdbb7 --- /dev/null +++ b/packages/backend/src/errorUtils.ts @@ -0,0 +1,15 @@ +// catch(err) always types err as unknown; this extracts a loggable message +// regardless of whether the thrown value was an Error or something else. +// Deliberately dependency-free (unlike Util.ts, which has a ServiceLocator +// side effect at import time) so importing it can't introduce new module +// load-order cycles. +export function getErrorMessage(err: unknown): string { + return err instanceof Error ? err.message : String(err); +} + +// Node/pg errors (e.g. ERR_STREAM_PREMATURE_CLOSE, Postgres's 42P01) attach a +// `code` string but aren't necessarily Error instances, so this narrows unknown +// catch values without assuming a specific error shape. +export function hasErrorCode(err: unknown, code: string): boolean { + return typeof err === 'object' && err !== null && 'code' in err && err.code === code; +} diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index 27b5b0a90..c62f80382 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -1,4 +1,4 @@ -require('dotenv').config(); +import 'dotenv/config'; import makeApp from './app'; import { setupSockets } from './socketHandler'; diff --git a/packages/backend/src/socketHandler.ts b/packages/backend/src/socketHandler.ts index c70cd356e..1043cbede 100644 --- a/packages/backend/src/socketHandler.ts +++ b/packages/backend/src/socketHandler.ts @@ -1,5 +1,6 @@ import express from 'express'; -import { Server } from 'socket.io'; +import { Server, Socket } from 'socket.io'; +import http from 'http'; import { innerGetGlobalElectionStats } from './Controllers/Election'; @@ -7,8 +8,8 @@ import { innerGetGlobalElectionStats } from './Controllers/Election'; export let io: Server|null = null; export const setupSockets = (app: express.Application) => { - const server = require('http').createServer(app) - + const server = http.createServer(app) + io = new Server(server, { cors: { @@ -16,7 +17,7 @@ export const setupSockets = (app: express.Application) => { } }) - io.on('connection', (socket: any) => { + io.on('connection', (socket: Socket) => { socket.on('join_landing_page', async () => { socket.join('landing_page'); socket.emit('updated_stats', await innerGetGlobalElectionStats(app.locals.req)); diff --git a/packages/backend/src/test/DBTest.ts b/packages/backend/src/test/DBTest.ts index 65519935d..9169fd90a 100644 --- a/packages/backend/src/test/DBTest.ts +++ b/packages/backend/src/test/DBTest.ts @@ -1,8 +1,7 @@ -import { parse } from "path/posix"; -import { assertNotNull, orDefault } from "../Util"; +import { orDefault } from "../Util"; import { DemoPGStore } from "./DemoPGStore"; -const { Pool } = require('pg'); +import { Pool } from "pg"; const pool = new Pool({ connectionString: process.env.DATABASE_URL, ssl: { @@ -16,7 +15,7 @@ export async function testDBCounter(): Promise { var myKey = "testDBCounter"; var demodb = new DemoPGStore(pool, "demopgstore"); - return demodb.init().then((_:any) => { + return demodb.init().then(() => { return demodb.get(myKey); }).then((num:string | null) => { return parseInt(orDefault(num, "0")); diff --git a/packages/backend/src/test/DemoPGStore.ts b/packages/backend/src/test/DemoPGStore.ts index fe5c123fe..4d58467f2 100644 --- a/packages/backend/src/test/DemoPGStore.ts +++ b/packages/backend/src/test/DemoPGStore.ts @@ -1,9 +1,11 @@ +import { Pool } from 'pg'; + export class DemoPGStore { - _postgresClient; + _postgresClient: Pool; _tableName:string; - constructor(client:any, tableName:string) { + constructor(client:Pool, tableName:string) { this._postgresClient = client; this._tableName = tableName; } @@ -14,12 +16,12 @@ export class DemoPGStore { CREATE TABLE IF NOT EXISTS ${this._tableName} ( id SERIAL PRIMARY KEY, key VARCHAR UNIQUE, - val VARCHAR + val VARCHAR ); `; console.info(query); var p = this._postgresClient.query(query); - return p.then((_: any) => { + return p.then(() => { return this; }); } @@ -37,7 +39,7 @@ export class DemoPGStore { text: sqlString, values: [key, value] }); - return p.then((res: any) => { + return p.then((res) => { console.info("set response rows: " + JSON.stringify(res)); return value; }); @@ -53,7 +55,7 @@ export class DemoPGStore { text: sqlString, values: [key] }); - return p.then((response: any) => { + return p.then((response) => { var rows = response.rows; if (rows.length == 0){ console.info(".get null"); @@ -73,7 +75,7 @@ export class DemoPGStore { text: sqlString, values: [key] }); - return p.then((response: any) => { + return p.then((response) => { if (response.rowCount == 1){ return true; } diff --git a/packages/backend/src/test/EmailTest.js b/packages/backend/src/test/EmailTest.js index c68e5e077..07a1c5f4a 100644 --- a/packages/backend/src/test/EmailTest.js +++ b/packages/backend/src/test/EmailTest.js @@ -13,6 +13,7 @@ // EmailService.sendEmails(msg) // EmailService.sendInvitations(election,voter,'https://localhost:3000') +/* eslint-disable @typescript-eslint/no-require-imports -- standalone CJS script, run directly with `node` */ require('dotenv').config() const sgMail = require('@sendgrid/mail') sgMail.setApiKey(process.env.SENDGRID_API_KEY) diff --git a/packages/backend/src/test/TestHelper.ts b/packages/backend/src/test/TestHelper.ts index 2bfed1f0b..61718ba96 100644 --- a/packages/backend/src/test/TestHelper.ts +++ b/packages/backend/src/test/TestHelper.ts @@ -6,9 +6,10 @@ import makeApp from "../app"; import Logger from "../Services/Logging/Logger"; import { TestLoggerImpl } from "../Services/Logging/TestLoggerImpl"; import ServiceLocator from "../ServiceLocator" -import { MockEventQueue } from "../Services/EventQueue/MockEventQueue"; import { candidate, rawVote } from "@equal-vote/star-vote-shared/domain_model/ITabulators"; -const request = require("supertest"); +import request, { Response as SupertestResponse, Test as SupertestTest } from "supertest"; +import EmailService from "../Services/Email/EmailService"; +import { MockEventQueue } from "../Services/EventQueue/MockEventQueue"; type ElectionResponse = { statusCode: number; @@ -38,14 +39,17 @@ export const mapMethodInputs = (names: string[], votes: (number | null)[][]): [c export class TestHelper { public expressApp; public logger: TestLoggerImpl; - public emailService: any; - public eventQueue:any; + public emailService: EmailService; + public eventQueue: Promise; private ctx = Logger.createContext("testHelper"); constructor() { this.emailService = ServiceLocator.emailService(); - this.eventQueue = ServiceLocator.eventQueue(); + // ServiceLocator is jest.mock()'d in tests (see setupTests.ts), and that mock's + // eventQueue() resolves to a MockEventQueue — narrower than the real module's + // declared Promise, which TS can't see through the mock swap. + this.eventQueue = ServiceLocator.eventQueue() as Promise; this.expressApp = makeApp(); this.logger = new TestLoggerImpl().setup(); } @@ -117,7 +121,7 @@ export class TestHelper { return this.electionResponse(res); } - private electionResponse(res: any): ElectionResponse { + private electionResponse(res: SupertestResponse): ElectionResponse { if (res.statusCode != 200) { return { statusCode: res.statusCode, @@ -155,7 +159,7 @@ export class TestHelper { electionId: Uid, ballot: Ballot | NewBallot, userToken: string | null - ): Promise { + ): Promise { return this.postRequest( `/API/Election/${electionId}/vote`, { ballot: ballot }, @@ -217,7 +221,7 @@ export class TestHelper { userToken: string | null, voterId: string | null, customToken: string| null = null - ): Promise { + ): Promise { var r = request(this.expressApp) .post(`/API/Election/${electionId}/vote`) .set("Accept", "application/json"); @@ -226,12 +230,25 @@ export class TestHelper { return r.send({ ballot: ballot }); } + async uploadBallots( + electionId: Uid, + ballots: Array<{ ballot: unknown; voter_id: string }>, + raceOrder: unknown[], + userToken: string | null + ): Promise { + return this.postRequest( + `/API/Election/${electionId}/uploadBallots`, + { ballots, race_order: raceOrder }, + userToken + ); + } + async submitElectionRoll( electionId: Uid, - electionRoll: any[], + electionRoll: unknown[], userToken: string | null, customToken: string| null = null - ): Promise { + ): Promise { var r = request(this.expressApp) .post(`/API/Election/${electionId}/rolls`) .set("Accept", "application/json"); @@ -244,7 +261,7 @@ export class TestHelper { electionId: Uid, userToken: string | null, customToken: string| null = null - ): Promise { + ): Promise { var r = request(this.expressApp) .delete(`/API/Election/${electionId}/rolls`) .set("Accept", "application/json"); @@ -257,7 +274,7 @@ export class TestHelper { electionId: Uid, userToken: string | null, customToken: string| null = null - ): Promise { + ): Promise { var r = request(this.expressApp) .get(`/API/Election/${electionId}/rolls`) .set("Accept", "application/json"); @@ -267,12 +284,12 @@ export class TestHelper { } private addUserTokenVoterIdCookie( - req: any, + req: SupertestTest, userToken: string | null, voterId: string | null, customToken: string | null, tempId: string | null, - ): any { + ): SupertestTest { var cookies = ""; if (userToken != null) { cookies = "id_token=" + userToken; diff --git a/packages/backend/src/test/accountService.test.ts b/packages/backend/src/test/accountService.test.ts index d490f76ac..36a68d65e 100644 --- a/packages/backend/src/test/accountService.test.ts +++ b/packages/backend/src/test/accountService.test.ts @@ -1,14 +1,15 @@ -require("dotenv").config(); -const request = require("supertest"); -var jwt = require("jsonwebtoken"); +import 'dotenv/config'; +import jwt from "jsonwebtoken"; -import { Election, electionValidation } from "@equal-vote/star-vote-shared/domain_model/Election"; import testInputs from "./testInputs"; import { TestHelper } from "./TestHelper"; import ServiceLocator from "../ServiceLocator"; +import MockAccountService from "../Services/Account/__mocks__/AccountService"; const th = new TestHelper(); -const accountService = ServiceLocator.accountService() as any; +// ServiceLocator is jest.mock()'d (see setupTests.ts), so accountService() actually +// returns the mock AccountService, which has a `verify` toggle the real class lacks. +const accountService = ServiceLocator.accountService() as unknown as MockAccountService; accountService.verify = true; afterEach(() => { diff --git a/packages/backend/src/test/anonymizedBallots.test.ts b/packages/backend/src/test/anonymizedBallots.test.ts index 758f578c1..d40e44890 100644 --- a/packages/backend/src/test/anonymizedBallots.test.ts +++ b/packages/backend/src/test/anonymizedBallots.test.ts @@ -1,4 +1,4 @@ -require("dotenv").config(); +import 'dotenv/config'; import { Election } from "@equal-vote/star-vote-shared/domain_model/Election"; import { NewBallot } from "@equal-vote/star-vote-shared/domain_model/Ballot"; @@ -82,7 +82,7 @@ describe("Anonymized ballots endpoint", () => { // All submitted scores come back, regardless of response order const scorePairs = res.body.ballots - .map((b: any) => b.votes[0].scores.map((s: any) => s.score)) + .map((b: {votes: {scores: {score: number}[]}[]}) => b.votes[0].scores.map((s) => s.score)) .sort(); expect(scorePairs).toEqual([[0, 5], [3, 2], [5, 0]].sort()); diff --git a/packages/backend/src/test/clearElectionRoll.test.ts b/packages/backend/src/test/clearElectionRoll.test.ts index f7b016f73..3a81a3d62 100644 --- a/packages/backend/src/test/clearElectionRoll.test.ts +++ b/packages/backend/src/test/clearElectionRoll.test.ts @@ -1,4 +1,4 @@ -require("dotenv").config(); +import 'dotenv/config'; import { TestHelper } from "./TestHelper"; import testInputs from "./testInputs"; diff --git a/packages/backend/src/test/createElection.test.ts b/packages/backend/src/test/createElection.test.ts index f985cfe37..59f63eeab 100644 --- a/packages/backend/src/test/createElection.test.ts +++ b/packages/backend/src/test/createElection.test.ts @@ -1,5 +1,4 @@ -require("dotenv").config(); -const request = require("supertest"); +import 'dotenv/config'; import { Election, electionValidation } from "@equal-vote/star-vote-shared/domain_model/Election"; import testInputs from "./testInputs"; diff --git a/packages/backend/src/test/customAuthKey.test.ts b/packages/backend/src/test/customAuthKey.test.ts index be35a8832..099490b44 100644 --- a/packages/backend/src/test/customAuthKey.test.ts +++ b/packages/backend/src/test/customAuthKey.test.ts @@ -1,17 +1,18 @@ -require("dotenv").config(); -const request = require("supertest"); +import 'dotenv/config'; -import { Election, electionValidation } from "@equal-vote/star-vote-shared/domain_model/Election"; import testInputs from "./testInputs"; import { TestHelper } from "./TestHelper"; import ServiceLocator from "../ServiceLocator"; +import MockAccountService from "../Services/Account/__mocks__/AccountService"; -var jwt = require('jsonwebtoken') -const crypto = require('crypto'); +import jwt from "jsonwebtoken"; +import crypto from "crypto"; const th = new TestHelper(); -const accountService = ServiceLocator.accountService() as any; +// ServiceLocator is jest.mock()'d (see setupTests.ts), so accountService() actually +// returns the mock AccountService, which has a `verify` toggle the real class lacks. +const accountService = ServiceLocator.accountService() as unknown as MockAccountService; accountService.verify = true; afterEach(() => { diff --git a/packages/backend/src/test/database_sandbox.ts b/packages/backend/src/test/database_sandbox.ts index 140daa967..3e39ad128 100644 --- a/packages/backend/src/test/database_sandbox.ts +++ b/packages/backend/src/test/database_sandbox.ts @@ -1,4 +1,5 @@ -require('dotenv').config() +/* eslint-disable @typescript-eslint/no-unused-vars -- manual scratch helpers, uncommented individually in RunTest() when needed */ +import 'dotenv/config'; import servicelocator from '../ServiceLocator' import { Election } from '@equal-vote/star-vote-shared/domain_model/Election' @@ -48,6 +49,7 @@ async function ResetDatabases() { await db.deleteFrom('electionRollDB').execute() } +// eslint-disable-next-line @typescript-eslint/no-explicit-any -- manual scratch helper; query is a Kysely query builder, whose generic type isn't worth pinning down here async function PrintQueryExpaination(query: any) { const explaination = await query.explain('json', sql`analyze`) console.info(JSON.stringify(explaination[0]['QUERY PLAN'], null, 4)) @@ -60,7 +62,7 @@ async function AddVoters(voter: ElectionRoll | ElectionRoll[]) { await db.insertInto('electionRollDB').values(voter).execute() } -async function GetLatestAll(i: string) { +async function GetLatestAll(_i: string) { const query = db .selectFrom("electionDB") .selectAll() diff --git a/packages/backend/src/test/editElection.test.ts b/packages/backend/src/test/editElection.test.ts index f0addeab8..266fb92c7 100644 --- a/packages/backend/src/test/editElection.test.ts +++ b/packages/backend/src/test/editElection.test.ts @@ -1,5 +1,4 @@ -require('dotenv').config(); -const request = require('supertest'); +import 'dotenv/config'; import { Election } from '@equal-vote/star-vote-shared/domain_model/Election'; import { TestHelper } from './TestHelper'; import testInputs from './testInputs'; diff --git a/packages/backend/src/test/emailRoll.test.ts b/packages/backend/src/test/emailRoll.test.ts index e7b361d53..5ec6435e1 100644 --- a/packages/backend/src/test/emailRoll.test.ts +++ b/packages/backend/src/test/emailRoll.test.ts @@ -1,5 +1,5 @@ -require("dotenv").config(); -const request = require("supertest"); +import 'dotenv/config'; +import request from "supertest"; import makeApp from "../app"; import { MockEventQueue } from "../Services/EventQueue/MockEventQueue"; import { TestHelper } from "./TestHelper"; diff --git a/packages/backend/src/test/finalizeElection.test.ts b/packages/backend/src/test/finalizeElection.test.ts index 506581e15..e5b771de4 100644 --- a/packages/backend/src/test/finalizeElection.test.ts +++ b/packages/backend/src/test/finalizeElection.test.ts @@ -1,5 +1,4 @@ -require("dotenv").config(); -const request = require("supertest"); +import 'dotenv/config'; import { TestHelper } from "./TestHelper"; import testInputs from "./testInputs"; diff --git a/packages/backend/src/test/idRoll.test.ts b/packages/backend/src/test/idRoll.test.ts index a45b0c049..52ff4bc06 100644 --- a/packages/backend/src/test/idRoll.test.ts +++ b/packages/backend/src/test/idRoll.test.ts @@ -1,6 +1,4 @@ -require('dotenv').config(); -const request = require('supertest'); -import { ElectionRoll, ElectionRollState } from '@equal-vote/star-vote-shared/domain_model/ElectionRoll'; +import 'dotenv/config'; import { MockEventQueue } from '../Services/EventQueue/MockEventQueue'; import { TestHelper } from './TestHelper'; import testInputs from './testInputs'; diff --git a/packages/backend/src/test/multiRaceElection.test.ts b/packages/backend/src/test/multiRaceElection.test.ts index 987143740..db82efd1f 100644 --- a/packages/backend/src/test/multiRaceElection.test.ts +++ b/packages/backend/src/test/multiRaceElection.test.ts @@ -1,5 +1,4 @@ -require("dotenv").config(); -const request = require("supertest"); +import 'dotenv/config'; import { Election, electionValidation } from "@equal-vote/star-vote-shared/domain_model/Election"; import testInputs from "./testInputs"; diff --git a/packages/backend/src/test/multiRaceResults.test.ts b/packages/backend/src/test/multiRaceResults.test.ts index 95cde5380..ce9d20713 100644 --- a/packages/backend/src/test/multiRaceResults.test.ts +++ b/packages/backend/src/test/multiRaceResults.test.ts @@ -1,4 +1,4 @@ -require("dotenv").config(); +import 'dotenv/config'; import { Election } from "@equal-vote/star-vote-shared/domain_model/Election"; import { NewBallot } from "@equal-vote/star-vote-shared/domain_model/Ballot"; @@ -122,9 +122,9 @@ describe("Multi Race Results", () => { const [race0, race1] = res.body.results; // the candidate lists must not have crossed over - expect(race0.summaryData.candidates.map((c: any) => c.name).sort()) + expect(race0.summaryData.candidates.map((c: {name: string}) => c.name).sort()) .toEqual(['Alice', 'Bob', 'Cara']); - expect(race1.summaryData.candidates.map((c: any) => c.name).sort()) + expect(race1.summaryData.candidates.map((c: {name: string}) => c.name).sort()) .toEqual(['Dan', 'Erin', 'Fay']); // nor the marks diff --git a/packages/backend/src/test/precinctElection.test.ts b/packages/backend/src/test/precinctElection.test.ts index 79641b6b7..c13398e8f 100644 --- a/packages/backend/src/test/precinctElection.test.ts +++ b/packages/backend/src/test/precinctElection.test.ts @@ -1,5 +1,4 @@ -require("dotenv").config(); -const request = require("supertest"); +import 'dotenv/config'; import { Election, electionValidation } from "@equal-vote/star-vote-shared/domain_model/Election"; import testInputs from "./testInputs"; diff --git a/packages/backend/src/test/sendGridWebhook.test.ts b/packages/backend/src/test/sendGridWebhook.test.ts index d6b6a9cb0..d0d34676a 100644 --- a/packages/backend/src/test/sendGridWebhook.test.ts +++ b/packages/backend/src/test/sendGridWebhook.test.ts @@ -1,6 +1,7 @@ -require("dotenv").config(); +import 'dotenv/config'; import crypto from 'crypto'; -const request = require("supertest"); +import request from "supertest"; +import express from 'express'; import makeApp from "../app"; import ServiceLocator from "../ServiceLocator"; import EmailEventsDB from "../Models/__mocks__/EmailEvents"; @@ -11,12 +12,11 @@ import EmailEventsDB from "../Models/__mocks__/EmailEvents"; const testKeyPair = crypto.generateKeyPairSync('ec', { namedCurve: 'prime256v1' }); const originalCreatePublicKey = crypto.createPublicKey; -jest.spyOn(crypto, 'createPublicKey').mockImplementation((...args: any[]) => { - const arg = args[0]; - if (arg && typeof arg === 'object' && arg.format === 'der' && arg.type === 'spki') { +jest.spyOn(crypto, 'createPublicKey').mockImplementation((key) => { + if (key && typeof key === 'object' && 'format' in key && key.format === 'der' && 'type' in key && key.type === 'spki') { return testKeyPair.publicKey; } - return originalCreatePublicKey.apply(crypto, args as any); + return originalCreatePublicKey(key); }); function signPayload(timestamp: string, body: string): string { @@ -25,7 +25,7 @@ function signPayload(timestamp: string, body: string): string { return sign.sign(testKeyPair.privateKey, 'base64'); } -function webhookPost(app: any, body: string, timestamp: string, signature: string) { +function webhookPost(app: express.Express, body: string, timestamp: string, signature: string) { return request(app) .post("/API/SendGridWebhook") .set("Content-Type", "application/json") @@ -103,9 +103,9 @@ describe("SendGrid Webhook", () => { expect(inserted.event_type).toBe("delivered"); expect(inserted.event_timestamp).toBe(new Date(1000 * 1000).toISOString()); // email should NOT be in details (PII) - expect((inserted.details as any)?.email).toBeUndefined(); + expect(inserted.details?.email).toBeUndefined(); // response should be in details - expect((inserted.details as any)?.response).toBe("250 OK"); + expect(inserted.details?.response).toBe("250 OK"); }); test("skips event when no sent row exists", async () => { diff --git a/packages/backend/src/test/testInputs.ts b/packages/backend/src/test/testInputs.ts index 2fa10a389..c68f5d5fb 100644 --- a/packages/backend/src/test/testInputs.ts +++ b/packages/backend/src/test/testInputs.ts @@ -1,9 +1,9 @@ -import { Ballot, NewBallot } from '@equal-vote/star-vote-shared/domain_model/Ballot'; +import { NewBallot } from '@equal-vote/star-vote-shared/domain_model/Ballot'; import { Election } from '@equal-vote/star-vote-shared/domain_model/Election'; import { ElectionSettings } from '@equal-vote/star-vote-shared/domain_model/ElectionSettings'; import { Race } from '@equal-vote/star-vote-shared/domain_model/Race'; -var jwt = require('jsonwebtoken') +import jwt from "jsonwebtoken"; export default { diff --git a/packages/backend/src/test/writeIns.test.ts b/packages/backend/src/test/writeIns.test.ts index 7fd6f4987..b4e8ce6a3 100644 --- a/packages/backend/src/test/writeIns.test.ts +++ b/packages/backend/src/test/writeIns.test.ts @@ -1,4 +1,4 @@ -require("dotenv").config(); +import 'dotenv/config'; import { Election } from "@equal-vote/star-vote-shared/domain_model/Election"; import { NewBallot } from "@equal-vote/star-vote-shared/domain_model/Ballot"; @@ -141,12 +141,12 @@ describe("Write-In Candidates", () => { }); test("Reject ballot with too many write-ins (>10)", async () => { - const scores = [ + const scores: { candidate_id: string, score: number, write_in_name?: string }[] = [ { candidate_id: '0', score: 1 }, { candidate_id: '1', score: 1 }, ]; for (let i = 0; i < 11; i++) { - scores.push({ candidate_id: `cwi-WriteIn${i}`, score: 1, write_in_name: `WriteIn${i}` } as any); + scores.push({ candidate_id: `cwi-WriteIn${i}`, score: 1, write_in_name: `WriteIn${i}` }); } const ballot: NewBallot = { election_id: election.election_id, @@ -216,7 +216,7 @@ describe("Write-In Candidates", () => { expect(res.statusCode).toBe(200); expect(res.body.write_in_data).toBeTruthy(); - const raceData = res.body.write_in_data.find((d: any) => d.race_id === 'race0'); + const raceData = res.body.write_in_data.find((d: {race_id: string}) => d.race_id === 'race0'); expect(raceData).toBeTruthy(); // We submitted 'Charlie' in 2 ballots, 'Dana' in 1 ballot expect(raceData.names['Charlie']).toBe(2); @@ -241,7 +241,7 @@ describe("Write-In Candidates", () => { expect(res.statusCode).toBe(200); expect(res.body.election).toBeTruthy(); - const race = res.body.election.races.find((r: any) => r.race_id === 'race0'); + const race = res.body.election.races.find((r: {race_id: string}) => r.race_id === 'race0'); expect(race.write_in_candidates).toHaveLength(2); expect(race.write_in_candidates[0].candidate_name).toBe('Charlie'); expect(race.write_in_candidates[0].approved).toBe(true); @@ -261,7 +261,7 @@ describe("Write-In Candidates", () => { const raceResult = res.body.results[0]; // Approved write-in 'Charlie' should appear as a candidate - const candidateNames = raceResult.summaryData.candidates.map((c: any) => c.name); + const candidateNames = raceResult.summaryData.candidates.map((c: {name: string}) => c.name); expect(candidateNames).toContain('Alice'); expect(candidateNames).toContain('Bob'); expect(candidateNames).toContain('Charlie'); @@ -301,7 +301,7 @@ describe("Write-In Candidates", () => { ); expect(res.statusCode).toBe(200); - const candidateNames = res.body.results[0].summaryData.candidates.map((c: any) => c.name); + const candidateNames = res.body.results[0].summaryData.candidates.map((c: {name: string}) => c.name); expect(candidateNames).toContain('Alice'); expect(candidateNames).toContain('Bob'); expect(candidateNames).not.toContain('Charlie'); @@ -332,7 +332,7 @@ describe("Zero-ballot paths", () => { ); expect(res.statusCode).toBe(200); expect(res.body.write_in_data).toBeTruthy(); - const raceData = res.body.write_in_data.find((d: any) => d.race_id === 'race0'); + const raceData = res.body.write_in_data.find((d: {race_id: string}) => d.race_id === 'race0'); expect(raceData).toBeTruthy(); expect(raceData.names).toEqual({}); th.testComplete(); diff --git a/packages/backend/src/untyped-modules.d.ts b/packages/backend/src/untyped-modules.d.ts new file mode 100644 index 000000000..f21325d31 --- /dev/null +++ b/packages/backend/src/untyped-modules.d.ts @@ -0,0 +1,6 @@ +// Ambient declarations for third-party packages with no published or installed types. +// Previously these were pulled in via `require(...)`, which is implicitly `any` and so +// never surfaced a missing-types error; declaring the module here preserves that same +// permissiveness while letting call sites use a real `import`. +declare module 'jsonwebtoken'; +declare module 'multer'; diff --git a/packages/backend/tsconfig.json b/packages/backend/tsconfig.json index 1814aa56b..34e3211bb 100644 --- a/packages/backend/tsconfig.json +++ b/packages/backend/tsconfig.json @@ -19,5 +19,10 @@ ], "resolveJsonModule": true, }, - "include": ["./src/**/*", "verifyShared.js"] + "include": ["./src/**/*", "verifyShared.js"], + "ts-node": { + // Without this, ts-node only type-checks each entry file's import graph and misses + // ambient .d.ts files (like untyped-modules.d.ts) that nothing explicitly imports. + "files": true + } } diff --git a/packages/backend/verifyShared.js b/packages/backend/verifyShared.js index c12ac391c..40d8add1f 100644 --- a/packages/backend/verifyShared.js +++ b/packages/backend/verifyShared.js @@ -1,5 +1,6 @@ +/* eslint-disable @typescript-eslint/no-require-imports -- standalone CJS script, run directly with `node` before the ts build exists */ try{ require("@equal-vote/star-vote-shared/config"); -}catch(e){ +}catch(_e){ throw "\n\nCould not find the shared BetterVoting module. Maybe you forgot to build it? Try this...\n\n npm run build -w @equal-vote/star-vote-shared\n\n" } diff --git a/packages/frontend/eslint.config.js b/packages/frontend/eslint.config.js index f26f4ae5e..271b9dcda 100644 --- a/packages/frontend/eslint.config.js +++ b/packages/frontend/eslint.config.js @@ -26,7 +26,8 @@ export default defineConfig([ { ignores: [ "**/node_modules/**", - "**/vite.config.ts" + "**/vite.config.ts", + "build/**" ] }, ]); diff --git a/packages/frontend/src/components/AuthSessionContextProvider.tsx b/packages/frontend/src/components/AuthSessionContextProvider.tsx index 652761beb..a1530d26d 100644 --- a/packages/frontend/src/components/AuthSessionContextProvider.tsx +++ b/packages/frontend/src/components/AuthSessionContextProvider.tsx @@ -35,7 +35,7 @@ export interface IAuthSession { const AuthSessionContext = createContext(null); export function AuthSessionContextProvider({ children }: { children: React.ReactNode }) { - const [accessToken, setAccessToken] = useCookie('access_token', null, 24 * 5) + const [, setAccessToken] = useCookie('access_token', null, 24 * 5) const [idToken, setIdToken] = useCookie('id_token', null, 24 * 5) const [refreshToken, setRefreshToken] = useCookie('refresh_token', null, 24 * 5) diff --git a/packages/frontend/src/components/Election/Admin/Admin.tsx b/packages/frontend/src/components/Election/Admin/Admin.tsx index aa4129fc8..0acbe30e2 100644 --- a/packages/frontend/src/components/Election/Admin/Admin.tsx +++ b/packages/frontend/src/components/Election/Admin/Admin.tsx @@ -25,7 +25,6 @@ const AdminPage = ({title, children}) => { const Admin = () => { const { id } = useParams(); - const {election} = useElection(); return ( diff --git a/packages/frontend/src/components/Election/Admin/PublishAndShare.tsx b/packages/frontend/src/components/Election/Admin/PublishAndShare.tsx index 789684a90..b791eac0b 100644 --- a/packages/frontend/src/components/Election/Admin/PublishAndShare.tsx +++ b/packages/frontend/src/components/Election/Admin/PublishAndShare.tsx @@ -1,19 +1,15 @@ -import { useState } from 'react'; -import { DateTime } from 'luxon'; import Grid from "@mui/material/Grid"; -import { Box, Divider, FormControl, FormHelperText, Input, InputLabel, MenuItem, Select, TextField } from "@mui/material"; +import { Box } from "@mui/material"; import { Typography } from "@mui/material"; -import { LinkButton, PrimaryButton, SecondaryButton } from "../../styles"; -import { Link, useNavigate } from 'react-router-dom'; +import { PrimaryButton } from "../../styles"; +import { useNavigate } from 'react-router-dom'; import ShareButton from "../ShareButton"; -import { useArchiveEleciton, useFinalizeElection, useSetOpenState } from "../../../hooks/useAPI"; -import { isValidDate, SwitchSetting, TransitionBox, useSubstitutedTranslation } from '../../util'; -import { dateToLocalLuxonDate, useEditElectionDetails } from '../../ElectionForm/Details/useEditElectionDetails'; +import { useFinalizeElection, useSetOpenState } from "../../../hooks/useAPI"; +import { SwitchSetting } from '../../util'; import useConfirm from '../../ConfirmationDialogProvider'; import useElection from '../../ElectionContextProvider'; import useAuthSession from '../../AuthSessionContextProvider'; import { AdminPageNavigation } from '../Sidebar'; -import { TimeZone, timeZones } from '@equal-vote/star-vote-shared/domain_model/Util'; import useOptimisticToggle from '~/hooks/useOptimisticToggle'; export default () => { diff --git a/packages/frontend/src/components/Election/ElectionStateWarning.tsx b/packages/frontend/src/components/Election/ElectionStateWarning.tsx index 9df9e3fae..bfbf8e1c0 100644 --- a/packages/frontend/src/components/Election/ElectionStateWarning.tsx +++ b/packages/frontend/src/components/Election/ElectionStateWarning.tsx @@ -1,10 +1,10 @@ -import { Box, Divider, Paper, Typography } from "@mui/material"; +import { Box, Paper, Typography } from "@mui/material"; import useElection from "../ElectionContextProvider"; import type { ElectionState } from "@equal-vote/star-vote-shared/domain_model/Election" import { ReportProblemOutlined } from "@mui/icons-material"; -export default function ElectionStateWarning - ({state, title, description, hideIcon=false, children}: {state?: ElectionState, title: string, description: string, hideIcon?: boolean, children?: any}) { +export default function ElectionStateWarning + ({state, title, description, hideIcon=false, children}: {state?: ElectionState, title: string, description: string, hideIcon?: boolean, children?: React.ReactNode}) { const { t, election } = useElection(); diff --git a/packages/frontend/src/components/Election/Results/IRV/winner.tsx b/packages/frontend/src/components/Election/Results/IRV/winner.tsx index 3f5512db1..a5c55dbf2 100644 --- a/packages/frontend/src/components/Election/Results/IRV/winner.tsx +++ b/packages/frontend/src/components/Election/Results/IRV/winner.tsx @@ -3,19 +3,17 @@ winner. */ -import Typography from '@mui/material/Typography'; import WidgetContainer from '../components/WidgetContainer'; import Widget from '../components/Widget'; import { irvCandidate, irvResults, - irvRoundResults } from "@equal-vote/star-vote-shared/domain_model/ITabulators"; import ResultsBarChart from "../components/ResultsBarChart"; import { irvContext, irvWinnerSearch } from "./ifc"; import useRace from '~/components/RaceContextProvider'; -export function IRVWinnerView ( {win, context}:{ +export function IRVWinnerView ( {win}:{ win: irvWinnerSearch, context: irvContext }) { let {results, t} = useRace(); diff --git a/packages/frontend/src/components/Election/Results/Results.tsx b/packages/frontend/src/components/Election/Results/Results.tsx index 7fb3df1f8..a1352bdbb 100644 --- a/packages/frontend/src/components/Election/Results/Results.tsx +++ b/packages/frontend/src/components/Election/Results/Results.tsx @@ -35,7 +35,6 @@ function STARResultsViewer({ filterRandomFromLogs }: {filterRandomFromLogs: bool const rounds = race.num_winners; const roundIndexes = Array.from({length: rounds}, () => i++); const flags = useFeatureFlags(); - const candidates = results.summaryData.candidates; results = results as starResults; @@ -251,7 +250,7 @@ function ApprovalResultsViewer() { } -function ResultsViewer({ methodKey, children }:{methodKey: string, children:ReactNode}) { +function ResultsViewer({ children }:{methodKey: string, children:ReactNode}) { return ( diff --git a/packages/frontend/src/components/Election/Results/STAR/STARDetailedResults.tsx b/packages/frontend/src/components/Election/Results/STAR/STARDetailedResults.tsx index 8152d35ce..8544ba2ac 100644 --- a/packages/frontend/src/components/Election/Results/STAR/STARDetailedResults.tsx +++ b/packages/frontend/src/components/Election/Results/STAR/STARDetailedResults.tsx @@ -3,7 +3,6 @@ import WidgetContainer from '../components/WidgetContainer'; import Widget from '../components/Widget'; import ResultsTable from '../components/ResultsTable'; import useRace from '~/components/RaceContextProvider'; -import { getEntry } from '@equal-vote/star-vote-shared/domain_model/Util'; import { formatPercent } from '~/components/util'; type candidateTableEntry = { diff --git a/packages/frontend/src/components/Election/Results/STAR/STAREqualPreferencesWidget.tsx b/packages/frontend/src/components/Election/Results/STAR/STAREqualPreferencesWidget.tsx index a4693141f..27371ef58 100644 --- a/packages/frontend/src/components/Election/Results/STAR/STAREqualPreferencesWidget.tsx +++ b/packages/frontend/src/components/Election/Results/STAR/STAREqualPreferencesWidget.tsx @@ -2,7 +2,6 @@ import useElection from "~/components/ElectionContextProvider"; import ResultsBarChart from "../components/ResultsBarChart" import Widget from "../components/Widget" import useAnonymizedBallots from "~/components/AnonymizedBallotsContextProvider"; -import { Candidate } from "@equal-vote/star-vote-shared/domain_model/Candidate"; import { getEntry } from "@equal-vote/star-vote-shared/domain_model/Util"; import { candidate } from "@equal-vote/star-vote-shared/domain_model/ITabulators"; diff --git a/packages/frontend/src/components/Election/Results/STAR/STARResultDetailedStepsWidget.tsx b/packages/frontend/src/components/Election/Results/STAR/STARResultDetailedStepsWidget.tsx index 59f3ba8f1..70c038e97 100644 --- a/packages/frontend/src/components/Election/Results/STAR/STARResultDetailedStepsWidget.tsx +++ b/packages/frontend/src/components/Election/Results/STAR/STARResultDetailedStepsWidget.tsx @@ -5,7 +5,7 @@ import Widget from '../components/Widget'; // NOTE: we're not using filterRandomFromLogs at the moment, but we'll add the functionality back later // eslint-disable-next-line @typescript-eslint/no-explicit-any -const STARResultDetailedStepsWidget = ({ results, rounds, t, filterRandomFromLogs}: {results: starResults, rounds: number, t: (key: string, v?: object) => any, filterRandomFromLogs: boolean }) => { +const STARResultDetailedStepsWidget = ({ results, rounds, t}: {results: starResults, rounds: number, t: (key: string, v?: object) => any, filterRandomFromLogs: boolean }) => { // make log groups const topLogs = [ diff --git a/packages/frontend/src/components/Election/Results/STAR/STARResultSummaryWidget.tsx b/packages/frontend/src/components/Election/Results/STAR/STARResultSummaryWidget.tsx index cccdc2bae..b05f3d76d 100644 --- a/packages/frontend/src/components/Election/Results/STAR/STARResultSummaryWidget.tsx +++ b/packages/frontend/src/components/Election/Results/STAR/STARResultSummaryWidget.tsx @@ -9,7 +9,6 @@ import WidgetContainer from '../components/WidgetContainer'; import Widget from '../components/Widget'; import ResultsBarChart from '../components/ResultsBarChart'; import ResultsPieChart from '../components/ResultsPieChart'; -import { getEntry } from '@equal-vote/star-vote-shared/domain_model/Util'; // eslint-disable-next-line @typescript-eslint/no-explicit-any const STARResultSummaryWidget = ({ results, roundIndex, t }: {results: starResults, roundIndex: number, t: (key: string, v?: object) => any }) => { diff --git a/packages/frontend/src/components/Election/Results/ViewElectionResults.tsx b/packages/frontend/src/components/Election/Results/ViewElectionResults.tsx index b12e73b06..8c30dc100 100644 --- a/packages/frontend/src/components/Election/Results/ViewElectionResults.tsx +++ b/packages/frontend/src/components/Election/Results/ViewElectionResults.tsx @@ -1,7 +1,7 @@ import { useEffect } from 'react'; import Results from './Results'; import Box from '@mui/material/Box'; -import { Divider, Typography } from "@mui/material"; +import { Typography } from "@mui/material"; import { useSubstitutedTranslation } from '../../util'; import { useGetResults } from '../../../hooks/useAPI'; import useElection from '../../ElectionContextProvider'; @@ -10,7 +10,6 @@ import ShareButton from '../ShareButton'; import { BallotDataExport } from './BallotDataExport'; import SupportBlurb from '../SupportBlurb'; import { Election } from '@equal-vote/star-vote-shared/domain_model/Election'; -import ElectionStateWarning from '../ElectionStateWarning'; import { AdminPageNavigation } from '../Sidebar'; import useFeatureFlags from '../../FeatureFlagContextProvider'; import { SecondaryButton } from '../../styles'; diff --git a/packages/frontend/src/components/Election/Results/components/HeadToHeadWidget.tsx b/packages/frontend/src/components/Election/Results/components/HeadToHeadWidget.tsx index 978eb31d9..3e848050c 100644 --- a/packages/frontend/src/components/Election/Results/components/HeadToHeadWidget.tsx +++ b/packages/frontend/src/components/Election/Results/components/HeadToHeadWidget.tsx @@ -1,11 +1,9 @@ -import useAnonymizedBallots from "~/components/AnonymizedBallotsContextProvider"; import useElection from "~/components/ElectionContextProvider"; import Widget from "./Widget"; import useRace from "~/components/RaceContextProvider"; import { useState } from "react"; import { Box, Divider, MenuItem, Select, Typography } from "@mui/material"; import { CHART_COLORS} from "~/components/util"; -import { Candidate } from "@equal-vote/star-vote-shared/domain_model/Candidate"; import HeadToHeadChart from "./HeadToHeadChart"; import ResultsKey from "./ResultsKey"; import { methodValueToTextKey } from "@equal-vote/star-vote-shared/domain_model/Race"; diff --git a/packages/frontend/src/components/Election/Results/components/ResultsPieChart.tsx b/packages/frontend/src/components/Election/Results/components/ResultsPieChart.tsx index 7ff8387c3..8295dad4c 100644 --- a/packages/frontend/src/components/Election/Results/components/ResultsPieChart.tsx +++ b/packages/frontend/src/components/Election/Results/components/ResultsPieChart.tsx @@ -1,5 +1,5 @@ import { useState } from "react"; -import { Cell, Legend, Pie, PieChart, ResponsiveContainer } from "recharts"; +import { Cell, Legend, Pie, PieChart, PieLabelRenderProps, ResponsiveContainer } from "recharts"; import { CHART_COLORS, truncName } from "~/components/util"; @@ -22,11 +22,11 @@ const ResultsPieChart = ({ data, colorOffset = 0, star = false, runoff = false, outerRadius, percent, index, - }: any) => { + }: PieLabelRenderProps) => { const RADIAN = Math.PI / 180; - const radius = innerRadius*.3 + outerRadius*.7; // bias toward the outside to give more space for the text - const x = cx + radius * Math.cos(-midAngle * RADIAN); - const y = cy + radius * Math.sin(-midAngle * RADIAN); + const radius = Number(innerRadius)*.3 + Number(outerRadius)*.7; // bias toward the outside to give more space for the text + const x = Number(cx) + radius * Math.cos(-Number(midAngle) * RADIAN); + const y = Number(cy) + radius * Math.sin(-Number(midAngle) * RADIAN); return ( - {rawNumbers? data[index].votes : `${(percent * 100).toFixed(0)}%`} + {rawNumbers? data[index].votes : `${(Number(percent) * 100).toFixed(0)}%`} ); }; diff --git a/packages/frontend/src/components/Election/Results/components/VoterIntentWidget.tsx b/packages/frontend/src/components/Election/Results/components/VoterIntentWidget.tsx index bcdf0a02a..5c42248cc 100644 --- a/packages/frontend/src/components/Election/Results/components/VoterIntentWidget.tsx +++ b/packages/frontend/src/components/Election/Results/components/VoterIntentWidget.tsx @@ -4,7 +4,7 @@ import Widget from "./Widget"; import useRace from "~/components/RaceContextProvider"; import { Box, Typography } from "@mui/material"; import ResultsPieChart from "./ResultsPieChart"; -import { candidate, irvResults } from "@equal-vote/star-vote-shared/domain_model/ITabulators"; +import { irvResults } from "@equal-vote/star-vote-shared/domain_model/ITabulators"; // eliminationOrder is an array of candidateIds const VoterIntentWidget = () => { diff --git a/packages/frontend/src/components/Election/Sidebar.tsx b/packages/frontend/src/components/Election/Sidebar.tsx index 8511313f3..8dafca47f 100644 --- a/packages/frontend/src/components/Election/Sidebar.tsx +++ b/packages/frontend/src/components/Election/Sidebar.tsx @@ -1,6 +1,6 @@ import Box from '@mui/material/Box'; import Typography from '@mui/material/Typography'; -import { Button, Divider, Grid } from '@mui/material'; +import { Button, Divider } from '@mui/material'; import { Link, useLocation, useNavigate } from 'react-router-dom'; import { Paper } from '@mui/material'; import useElection from '../ElectionContextProvider'; @@ -31,7 +31,7 @@ function useAdminPages() { ]; } -const ListItem = ({ text, link, icon, isActive }: { text:string, link: string, icon: any, isActive?: boolean }) => { +const ListItem = ({ text, link, icon, isActive }: { text:string, link: string, icon: React.ReactNode, isActive?: boolean }) => { return (