From 184cc6f38adf1b027de8b24def66884da258c2bb Mon Sep 17 00:00:00 2001 From: eznarf <41272412+eznarf@users.noreply.github.com> Date: Sat, 27 Jan 2024 14:58:02 -0800 Subject: [PATCH 1/9] Documenting starting up DB with docker --- docker-compose.yml | 10 ++++++++ docs/contributions/1_local_setup.md | 40 +++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+) diff --git a/docker-compose.yml b/docker-compose.yml index 9fe01884..cd5a31ab 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -19,5 +19,15 @@ services: - POSTGRES_PASSWORD=ChangeMeOrDontTest2020 restart: on-failure + keycloak: + image: quay.io/keycloak/keycloak:23.0.1 + command: + - start-dev + - --import-realm + ports: + - '8080:8080' + environment: + - KEYCLOAK_ADMIN=admin + - KEYCLOAK_ADMIN_PASSWORD=admin networks: star-net: {} diff --git a/docs/contributions/1_local_setup.md b/docs/contributions/1_local_setup.md index ea50a783..57e7c7c8 100644 --- a/docs/contributions/1_local_setup.md +++ b/docs/contributions/1_local_setup.md @@ -206,6 +206,46 @@ cd frontend npm start ``` +## Hosting database and Keycloak locally + +If you want to make changes to database schemas or keycloak settings, or don't have access to dev credentials, you can host the database and keycloak server locally with Docker. + +Follow the instructions [here](https://docs.docker.com/engine/install/) to install docker, check system requirements for turning on WSL if using Windows. After installed start Docker Desktop. + +docker-compose.yml in the project directory contains the configuration launching the server, database, and keycloak. Not much work has been done yet on running all three together, however you can launch the database with + +```bash +docker compose -f "docker-compose.yml" up -d --build my-db +``` + +Next, update the database variables in your backend .env with + +```bash +DATABASE_URL=postgresql://postgres:ChangeMeOrDontTest2020@localhost:5432/postgres +DEV_DATABASE=FALSE +``` + +and run the commands + +```bash +cd backend +npm run build +npm run migrate:latest +``` + +Migrate:latest will initialize the database tables with the latest migrations. + +To run keycloak: + +```bash +docker compose -f "docker-compose.yml" up -d --build keycloak +``` + +You can then access keycloak at http://localhost:8080/ + +See the keycloak [deployment](contributions/Infrastructure/10_keycloak_deployment.md) and [configuration](contributions/Infrastructure/11_keycloak_configuration.md) documentation for next steps. + + ## Login Deploying to localhost still uses the same KeyCloak userbase as production (at least for now). If you want to login to the production keycloak make sure you followed the keycloak step in [the environment variable setup](#step-1-set-up-the-environment-variable-file). That said logging in through localhost does require some extra steps, so be sure to follow these additional steps From b66b5be86604ce65ff95f05e1c77737e1dcfacc4 Mon Sep 17 00:00:00 2001 From: eznarf <41272412+eznarf@users.noreply.github.com> Date: Sat, 27 Jan 2024 17:31:34 -0800 Subject: [PATCH 2/9] Adding up and down database migration scripts --- backend/src/Migrators/migrate-down.ts | 10 +++++ backend/src/Migrators/migrate-to-latest.ts | 11 ++++++ backend/src/Migrators/migrate-up.ts | 10 +++++ backend/src/Migrators/migration-utils.ts | 42 ++++++++++++++++++++ backend/src/migrate-to-latest.ts | 46 ---------------------- 5 files changed, 73 insertions(+), 46 deletions(-) create mode 100644 backend/src/Migrators/migrate-down.ts create mode 100644 backend/src/Migrators/migrate-to-latest.ts create mode 100644 backend/src/Migrators/migrate-up.ts create mode 100644 backend/src/Migrators/migration-utils.ts delete mode 100644 backend/src/migrate-to-latest.ts diff --git a/backend/src/Migrators/migrate-down.ts b/backend/src/Migrators/migrate-down.ts new file mode 100644 index 00000000..83c9c5a9 --- /dev/null +++ b/backend/src/Migrators/migrate-down.ts @@ -0,0 +1,10 @@ +import { createMigrator, handleMigration } from "./migration-utils" + +async function migrateToLatest() { + const { migrator, db } = createMigrator() + handleMigration(await migrator.migrateDown()) + + await db.destroy() + } + + migrateToLatest() \ No newline at end of file diff --git a/backend/src/Migrators/migrate-to-latest.ts b/backend/src/Migrators/migrate-to-latest.ts new file mode 100644 index 00000000..5fff456f --- /dev/null +++ b/backend/src/Migrators/migrate-to-latest.ts @@ -0,0 +1,11 @@ +require('dotenv').config() +import { createMigrator, handleMigration } from "./migration-utils" + +async function migrateToLatest() { + const { migrator, db } = createMigrator() + handleMigration(await migrator.migrateToLatest()) + + await db.destroy() +} + +migrateToLatest() \ No newline at end of file diff --git a/backend/src/Migrators/migrate-up.ts b/backend/src/Migrators/migrate-up.ts new file mode 100644 index 00000000..23602559 --- /dev/null +++ b/backend/src/Migrators/migrate-up.ts @@ -0,0 +1,10 @@ +import { createMigrator, handleMigration } from "./migration-utils" + +async function migrateToLatest() { + const { migrator, db } = createMigrator() + handleMigration(await migrator.migrateUp()) + + await db.destroy() + } + + migrateToLatest() \ No newline at end of file diff --git a/backend/src/Migrators/migration-utils.ts b/backend/src/Migrators/migration-utils.ts new file mode 100644 index 00000000..47a0b528 --- /dev/null +++ b/backend/src/Migrators/migration-utils.ts @@ -0,0 +1,42 @@ +import * as path from 'path' +import { promises as fs } from 'fs' +import { + Migrator, + FileMigrationProvider, + MigrationResultSet, +} from 'kysely' + +import servicelocator from '../ServiceLocator' + +export function createMigrator() { + const db = servicelocator.database() + const migrator = new Migrator({ + db, + provider: new FileMigrationProvider({ + fs, + path, + // This needs to be an absolute path. + migrationFolder: path.join(__dirname, './Migrations'), + }), + }) + return { + migrator, + db + } +} + +export async function handleMigration(migrationResult: MigrationResultSet) { + migrationResult.results?.forEach((it) => { + if (it.status === 'Success') { + console.log(`migration "${it.migrationName}" was executed successfully`) + } else if (it.status === 'Error') { + console.error(`failed to execute migration "${it.migrationName}"`) + } + }) + + if (migrationResult.error) { + console.error('failed to migrate') + console.error(migrationResult.error) + process.exit(1) + } +} \ No newline at end of file diff --git a/backend/src/migrate-to-latest.ts b/backend/src/migrate-to-latest.ts deleted file mode 100644 index a4ec0abd..00000000 --- a/backend/src/migrate-to-latest.ts +++ /dev/null @@ -1,46 +0,0 @@ -require('dotenv').config() -import * as path from 'path' -import { Pool } from 'pg' -import { promises as fs } from 'fs' -import { - Kysely, - Migrator, - PostgresDialect, - FileMigrationProvider, -} from 'kysely' - -import servicelocator from './ServiceLocator' - -async function migrateToLatest() { - const db = servicelocator.database() - - const migrator = new Migrator({ - db, - provider: new FileMigrationProvider({ - fs, - path, - // This needs to be an absolute path. - migrationFolder: path.join(__dirname, './Migrations'), - }), - }) - - const { error, results } = await migrator.migrateToLatest() - - results?.forEach((it) => { - if (it.status === 'Success') { - console.log(`migration "${it.migrationName}" was executed successfully`) - } else if (it.status === 'Error') { - console.error(`failed to execute migration "${it.migrationName}"`) - } - }) - - if (error) { - console.error('failed to migrate') - console.error(error) - process.exit(1) - } - - await db.destroy() -} - -migrateToLatest() \ No newline at end of file From c22303ea6156f36ee4dac6e35618ecd9dd2fd713 Mon Sep 17 00:00:00 2001 From: eznarf <41272412+eznarf@users.noreply.github.com> Date: Sat, 27 Jan 2024 17:33:53 -0800 Subject: [PATCH 3/9] Updating npm package with new scripts --- backend/package.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/backend/package.json b/backend/package.json index 1a209022..cf08c986 100644 --- a/backend/package.json +++ b/backend/package.json @@ -8,7 +8,9 @@ "start": "npm run-script build && node ./build/backend/src/index.js", "dev": "nodemon ./src/index.ts", "build": "tsc --project ./", - "migrate:latest": "node ./build/backend/src/migrate-to-latest.js" + "migrate:latest": "node ./build/backend/src/Migrators/migrate-to-latest.js", + "migrate:up": "node ./build/backend/src/Migrators/migrate-down.js", + "migrate:down": "node ./build/backend/src/Migrators/migrate-to-latest.js" }, "keywords": [], "author": "", From bb851bc590a71763696de271ce984c37c24c17e9 Mon Sep 17 00:00:00 2001 From: eznarf <41272412+eznarf@users.noreply.github.com> Date: Sat, 27 Jan 2024 17:36:00 -0800 Subject: [PATCH 4/9] Adding create date, claim key, is public flag. Changes ip address to hash. --- .../src/Migrations/2024_01_27_Create_Date.ts | 35 +++++++++++++++++++ domain_model/Election.ts | 3 ++ domain_model/ElectionRoll.ts | 2 +- 3 files changed, 39 insertions(+), 1 deletion(-) create mode 100644 backend/src/Migrations/2024_01_27_Create_Date.ts diff --git a/backend/src/Migrations/2024_01_27_Create_Date.ts b/backend/src/Migrations/2024_01_27_Create_Date.ts new file mode 100644 index 00000000..0b7640e0 --- /dev/null +++ b/backend/src/Migrations/2024_01_27_Create_Date.ts @@ -0,0 +1,35 @@ +import { Kysely } from 'kysely' + +export async function up(db: Kysely): Promise { + await db.schema.alterTable('electionDB') + .addColumn('claim_key_hash','varchar') + .addColumn('is_public', 'boolean') + .addColumn('create_date', 'varchar') + .execute() + + await db.updateTable('electionDB') + .set({create_date: Date.now().toString()}) + .execute() + + await db.schema.alterTable('electionDB') + .alterColumn('create_date',(col) => col.setNotNull()) + .execute() + + await db.schema.alterTable('electionRollDB') + .dropColumn('ip_address') + .addColumn('ip_hash','varchar') + .execute() +} + +export async function down(db: Kysely): Promise { + await db.schema.alterTable('electionDB') + .dropColumn('claim_key_hash') + .dropColumn('is_public') + .dropColumn('create_date') + .execute() + + await db.schema.alterTable('electionRollDB') + .dropColumn('ip_hash') + .addColumn('ip_address','varchar') + .execute() + } \ No newline at end of file diff --git a/domain_model/Election.ts b/domain_model/Election.ts index 340cf473..b9e32295 100644 --- a/domain_model/Election.ts +++ b/domain_model/Election.ts @@ -19,6 +19,9 @@ export interface Election { races: Race[]; // one or more race definitions settings: ElectionSettings; auth_key?: string; + claim_key_hash: string; + is_public: Boolean; + create_date: Date | string; } diff --git a/domain_model/ElectionRoll.ts b/domain_model/ElectionRoll.ts index 3ba737a3..48c5d030 100644 --- a/domain_model/ElectionRoll.ts +++ b/domain_model/ElectionRoll.ts @@ -7,7 +7,7 @@ export interface ElectionRoll { email?: string; // Email address of voter submitted: boolean; //has ballot been submitted ballot_id?: Uid; //ID of ballot, unsure if this is needed - ip_address?: string; //IP Address of voter + ip_hash?: string; //IP Address of voter address?: string; // Address of voter state: ElectionRollState; //state of election roll history?: ElectionRollAction[];// history of changes to election roll From 466439959d9d5a3f4559784a2676e2064fff12d4 Mon Sep 17 00:00:00 2001 From: mikefranze <41272412+mikefranze@users.noreply.github.com> Date: Sat, 27 Jan 2024 21:56:31 -0800 Subject: [PATCH 5/9] Fixing various bugs --- backend/package.json | 4 ++-- backend/src/Migrations/2024_01_27_Create_Date.ts | 2 +- backend/src/Migrators/migrate-down.ts | 7 ++++--- backend/src/Migrators/migrate-up.ts | 5 +++-- backend/src/Migrators/migration-utils.ts | 2 +- backend/src/Models/Elections.ts | 2 +- domain_model/Election.ts | 4 ++-- 7 files changed, 14 insertions(+), 12 deletions(-) diff --git a/backend/package.json b/backend/package.json index cf08c986..c1f89121 100644 --- a/backend/package.json +++ b/backend/package.json @@ -9,8 +9,8 @@ "dev": "nodemon ./src/index.ts", "build": "tsc --project ./", "migrate:latest": "node ./build/backend/src/Migrators/migrate-to-latest.js", - "migrate:up": "node ./build/backend/src/Migrators/migrate-down.js", - "migrate:down": "node ./build/backend/src/Migrators/migrate-to-latest.js" + "migrate:up": "node ./build/backend/src/Migrators/migrate-up.js", + "migrate:down": "node ./build/backend/src/Migrators/migrate-down.js" }, "keywords": [], "author": "", diff --git a/backend/src/Migrations/2024_01_27_Create_Date.ts b/backend/src/Migrations/2024_01_27_Create_Date.ts index 0b7640e0..261407ba 100644 --- a/backend/src/Migrations/2024_01_27_Create_Date.ts +++ b/backend/src/Migrations/2024_01_27_Create_Date.ts @@ -8,7 +8,7 @@ export async function up(db: Kysely): Promise { .execute() await db.updateTable('electionDB') - .set({create_date: Date.now().toString()}) + .set({create_date: new Date().toISOString()}) .execute() await db.schema.alterTable('electionDB') diff --git a/backend/src/Migrators/migrate-down.ts b/backend/src/Migrators/migrate-down.ts index 83c9c5a9..6661edd3 100644 --- a/backend/src/Migrators/migrate-down.ts +++ b/backend/src/Migrators/migrate-down.ts @@ -1,10 +1,11 @@ +require('dotenv').config() import { createMigrator, handleMigration } from "./migration-utils" -async function migrateToLatest() { +async function migrateDown() { const { migrator, db } = createMigrator() handleMigration(await migrator.migrateDown()) - + await db.destroy() } - migrateToLatest() \ No newline at end of file + migrateDown() \ No newline at end of file diff --git a/backend/src/Migrators/migrate-up.ts b/backend/src/Migrators/migrate-up.ts index 23602559..b713a78a 100644 --- a/backend/src/Migrators/migrate-up.ts +++ b/backend/src/Migrators/migrate-up.ts @@ -1,10 +1,11 @@ +require('dotenv').config() import { createMigrator, handleMigration } from "./migration-utils" -async function migrateToLatest() { +async function migrateUp() { const { migrator, db } = createMigrator() handleMigration(await migrator.migrateUp()) await db.destroy() } - migrateToLatest() \ No newline at end of file + migrateUp() \ No newline at end of file diff --git a/backend/src/Migrators/migration-utils.ts b/backend/src/Migrators/migration-utils.ts index 47a0b528..6d3f0b93 100644 --- a/backend/src/Migrators/migration-utils.ts +++ b/backend/src/Migrators/migration-utils.ts @@ -16,7 +16,7 @@ export function createMigrator() { fs, path, // This needs to be an absolute path. - migrationFolder: path.join(__dirname, './Migrations'), + migrationFolder: path.join(__dirname, '../Migrations'), }), }) return { diff --git a/backend/src/Models/Elections.ts b/backend/src/Models/Elections.ts index 3cec2b10..88d0c5ad 100644 --- a/backend/src/Models/Elections.ts +++ b/backend/src/Models/Elections.ts @@ -28,7 +28,7 @@ export default class ElectionsDB { createElection(election: Election, ctx: ILoggingContext, reason: string): Promise { Logger.debug(ctx, `${tableName}.createElection`, election); - + election.create_date = new Date().toISOString() const newElection = this._postgresClient .insertInto(tableName) .values(election) diff --git a/domain_model/Election.ts b/domain_model/Election.ts index b9e32295..9cf55262 100644 --- a/domain_model/Election.ts +++ b/domain_model/Election.ts @@ -19,8 +19,8 @@ export interface Election { races: Race[]; // one or more race definitions settings: ElectionSettings; auth_key?: string; - claim_key_hash: string; - is_public: Boolean; + claim_key_hash?: string; + is_public?: Boolean; create_date: Date | string; } From d03e927f2c10d75d2c6b88cb4d36ca66860286fb Mon Sep 17 00:00:00 2001 From: mikefranze <41272412+mikefranze@users.noreply.github.com> Date: Sat, 27 Jan 2024 23:09:51 -0800 Subject: [PATCH 6/9] Creating a NewElection type without properties assigned by server --- domain_model/Election.ts | 3 +++ .../ElectionForm/CreateElectionTemplates.tsx | 11 +++++------ frontend/src/components/ElectionForm/QuickPoll.tsx | 7 +++---- frontend/src/hooks/useAPI.ts | 4 ++-- 4 files changed, 13 insertions(+), 12 deletions(-) diff --git a/domain_model/Election.ts b/domain_model/Election.ts index 9cf55262..177b8538 100644 --- a/domain_model/Election.ts +++ b/domain_model/Election.ts @@ -23,7 +23,10 @@ export interface Election { is_public?: Boolean; create_date: Date | string; } +type Omit = Pick> +type PartialBy = Omit & Partial> +export interface NewElection extends PartialBy {} export function electionValidation(obj:Election): string | null { if (!obj){ diff --git a/frontend/src/components/ElectionForm/CreateElectionTemplates.tsx b/frontend/src/components/ElectionForm/CreateElectionTemplates.tsx index 861e5827..095ada85 100644 --- a/frontend/src/components/ElectionForm/CreateElectionTemplates.tsx +++ b/frontend/src/components/ElectionForm/CreateElectionTemplates.tsx @@ -1,6 +1,6 @@ import React from 'react' import { useNavigate } from "react-router" -import { Election } from '@domain_model/Election'; +import { Election, NewElection } from '@domain_model/Election'; import { usePostElection } from '../../hooks/useAPI'; import { DateTime } from 'luxon' import { Card, CardActionArea, CardMedia, CardContent, Typography, Box, Grid } from '@mui/material'; @@ -14,15 +14,14 @@ const CreateElectionTemplates = () => { const themeSelector = useThemeSelector() const navigate = useNavigate() const { error, isPending, makeRequest: postElection } = usePostElection() - const [quickPoll, setQuickPoll] = useLocalStorage('QuickPoll', null) + const [quickPoll, setQuickPoll] = useLocalStorage('QuickPoll', null) const cardColor = themeSelector.mode === 'darkMode' ? 'brand.gray5' : 'brand.gray1' - const defaultElection: Election = { + const defaultElection: NewElection = { title: '', - election_id: '0', + owner_id: '', description: '', state: 'draft', frontend_url: '', - owner_id: '', races: [], settings: { voter_access: 'open', @@ -37,7 +36,7 @@ const CreateElectionTemplates = () => { } } - const onAddElection = async (updateFunc: (election: Election) => any) => { + const onAddElection = async (updateFunc: (election: NewElection) => any) => { const election = defaultElection updateFunc(election) // calls post election api, throws error if response not ok diff --git a/frontend/src/components/ElectionForm/QuickPoll.tsx b/frontend/src/components/ElectionForm/QuickPoll.tsx index 72fe8d9e..38e94cbe 100644 --- a/frontend/src/components/ElectionForm/QuickPoll.tsx +++ b/frontend/src/components/ElectionForm/QuickPoll.tsx @@ -11,16 +11,15 @@ import { useLocalStorage } from '../../hooks/useLocalStorage'; import Typography from '@mui/material/Typography'; import { usePostElection } from '../../hooks/useAPI'; import { useCookie } from '../../hooks/useCookie'; -import { Election } from '@domain_model/Election.js'; +import { Election, NewElection } from '@domain_model/Election.js'; const QuickPoll = ({ authSession }) => { const [tempID, setTempID] = useCookie('temp_id', '0') const navigate = useNavigate() const { error, isPending, makeRequest: postElection } = usePostElection() - const QuickPollTemplate: Election = { + const QuickPollTemplate: NewElection = { title: '', - election_id: '0', state: 'open', frontend_url: '', owner_id: '0', @@ -60,7 +59,7 @@ const QuickPoll = ({ authSession }) => { } - const [election, setElectionData] = useLocalStorage('QuickPoll', QuickPollTemplate) + const [election, setElectionData] = useLocalStorage('QuickPoll', QuickPollTemplate) const [titleError, setTitleError] = useState(false) const onSubmitElection = async (election) => { // calls post election api, throws error if response not ok diff --git a/frontend/src/hooks/useAPI.ts b/frontend/src/hooks/useAPI.ts index 3b443fd9..ff39ffd2 100644 --- a/frontend/src/hooks/useAPI.ts +++ b/frontend/src/hooks/useAPI.ts @@ -1,4 +1,4 @@ -import { Election } from "@domain_model/Election"; +import { Election, NewElection } from "@domain_model/Election"; import { VoterAuth } from '@domain_model/VoterAuth'; import { ElectionRoll } from "@domain_model/ElectionRoll"; import useFetch from "./useFetch"; @@ -20,7 +20,7 @@ export const useGetElections = () => { } export const usePostElection = () => { - return useFetch<{ Election: Election }, { election: Election }>('/API/Elections', 'post') + return useFetch<{ Election: NewElection }, { election: Election }>('/API/Elections', 'post') } export const useEditElection = (election_id: string | undefined) => { From 0e1b56e26d6c5d8ddc1147051b2d02f5d93dd0b5 Mon Sep 17 00:00:00 2001 From: mikefranze <41272412+mikefranze@users.noreply.github.com> Date: Sat, 27 Jan 2024 23:15:38 -0800 Subject: [PATCH 7/9] Switching ip_address to ip_hash --- backend/src/Controllers/controllerUtils.ts | 8 +++++-- .../Controllers/registerVoterController.ts | 3 ++- backend/src/Controllers/voterRollUtils.ts | 15 ++++++------ .../src/Migrations/2024_01_27_Create_Date.ts | 24 ++++++++++++++----- backend/src/Models/CastVoteStore.ts | 3 +-- backend/src/Models/ElectionRolls.ts | 6 ++--- backend/src/Models/Elections.ts | 1 - backend/src/Models/IElectionRollStore.ts | 2 +- backend/src/Models/__mocks__/ElectionRolls.ts | 6 ++--- domain_model/Ballot.ts | 2 +- .../Election/Admin/ViewElectionRolls.tsx | 2 +- 11 files changed, 44 insertions(+), 28 deletions(-) diff --git a/backend/src/Controllers/controllerUtils.ts b/backend/src/Controllers/controllerUtils.ts index e9ab989a..ac2dd097 100644 --- a/backend/src/Controllers/controllerUtils.ts +++ b/backend/src/Controllers/controllerUtils.ts @@ -5,14 +5,14 @@ import { BadRequest, InternalServerError, Unauthorized } from "@curveball/http-e import { Request, Response } from 'express'; import { roles } from "../../../domain_model/roles"; import { hasPermission, permission, permissions } from '../../../domain_model/permissions'; -import { randomUUID } from "crypto"; +import { randomUUID, createHash } from "crypto"; import ServiceLocator from "../ServiceLocator"; - const accountService = ServiceLocator.accountService(); export function expectValidElectionFromRequest(req:IRequest):Election { const inputElection = req.body.Election; inputElection.election_id = randomUUID(); + inputElection.create_date = new Date().toISOString() const validationErr = electionValidation(inputElection); if (validationErr) { Logger.info(req, "Invalid Election: " + validationErr, inputElection); @@ -38,4 +38,8 @@ export function expectPermission(roles:roles[],permission:permission):any { if (!roles.some( (role) => permission.includes(role))){ throw new Unauthorized("Does not have permission") } +} + +export function hashString(inputString: string) { + return createHash('sha1').update(inputString).digest('hex') } \ No newline at end of file diff --git a/backend/src/Controllers/registerVoterController.ts b/backend/src/Controllers/registerVoterController.ts index be6d467b..47bc8664 100644 --- a/backend/src/Controllers/registerVoterController.ts +++ b/backend/src/Controllers/registerVoterController.ts @@ -10,6 +10,7 @@ import { Election } from "../../../domain_model/Election"; import { randomUUID } from "crypto"; import { IElectionRequest } from "../IRequest"; import { Response, NextFunction } from 'express'; +import { hashString } from "./controllerUtils"; const registerVoter = async (req: IElectionRequest, res: Response, next: NextFunction) => { Logger.info(req, `${className}.registerVoter ${req.election.election_id}`); @@ -43,7 +44,7 @@ const registerVoter = async (req: IElectionRequest, res: Response, next: NextFun election_id: req.election.election_id, email: req.user?.email, submitted: false, - ip_address: targetElection.settings.voter_authentication.ip_address ? req.ip : undefined, + ip_hash: targetElection.settings.voter_authentication.ip_address ? hashString(req.ip) : undefined, state: ElectionRollState.registered, history: [], registration: req.body.registration, diff --git a/backend/src/Controllers/voterRollUtils.ts b/backend/src/Controllers/voterRollUtils.ts index 755e985c..5c180d8e 100644 --- a/backend/src/Controllers/voterRollUtils.ts +++ b/backend/src/Controllers/voterRollUtils.ts @@ -6,6 +6,7 @@ import Logger from "../Services/Logging/Logger"; import { BadRequest, InternalServerError, Unauthorized } from "@curveball/http-errors"; import { ILoggingContext } from "../Services/Logging/ILogger"; import { randomUUID } from "crypto"; +import { hashString } from "./controllerUtils"; const ElectionRollModel = ServiceLocator.electionRollDb(); @@ -14,7 +15,7 @@ export async function getOrCreateElectionRoll(req: IRequest, election: Election, Logger.info(req, `getOrCreateElectionRoll`) // Get data that is used for voter authentication - const ip_address = election.settings.voter_authentication.ip_address ? req.ip : null + const ip_hash = election.settings.voter_authentication.ip_address ? hashString(req.ip) : null const email = election.settings.voter_authentication.email ? req.user?.email : null let voter_id = election.settings.voter_authentication.voter_id ? req.cookies?.voter_id : null @@ -22,8 +23,8 @@ export async function getOrCreateElectionRoll(req: IRequest, election: Election, // This is an odd way of going about this, rather than getting a roll that matches all three we get all that match any of the fields and // check the output for a number of edge cases. var electionRollEntries = null - if ((ip_address || email || voter_id)) { - electionRollEntries = await ElectionRollModel.getElectionRoll(String(election.election_id), voter_id, email, ip_address, ctx); + if ((ip_hash || email || voter_id)) { + electionRollEntries = await ElectionRollModel.getElectionRoll(String(election.election_id), voter_id, email, ip_hash, ctx); } @@ -46,12 +47,12 @@ export async function getOrCreateElectionRoll(req: IRequest, election: Election, election_id: String(election.election_id), email: req.user?.email ? req.user.email : undefined, voter_id: new_voter_id, - ip_address: req.ip, + ip_hash: hashString(req.ip), submitted: false, state: ElectionRollState.approved, history: history, }] - if ((ip_address || email || voter_id)) { + if ((ip_hash || email || voter_id)) { const newElectionRoll = await ElectionRollModel.submitElectionRoll(roll, ctx, `User requesting Roll and is authorized`) if (!newElectionRoll) { Logger.error(ctx, "Failed to update ElectionRoll"); @@ -71,8 +72,8 @@ export async function getOrCreateElectionRoll(req: IRequest, election: Election, Logger.error(req, "Multiple election roll entries found", electionRollEntries); throw new InternalServerError('Multiple election roll entries found'); } - if (election.settings.voter_authentication.ip_address && electionRollEntries[0].ip_address) { - if (electionRollEntries[0].ip_address !== ip_address) { + if (election.settings.voter_authentication.ip_address && electionRollEntries[0].ip_hash) { + if (electionRollEntries[0].ip_hash !== ip_hash) { Logger.error(req, "IP Address does not match saved voter roll", electionRollEntries); throw new Unauthorized('IP Address does not match saved voter roll'); } diff --git a/backend/src/Migrations/2024_01_27_Create_Date.ts b/backend/src/Migrations/2024_01_27_Create_Date.ts index 261407ba..3ef392b8 100644 --- a/backend/src/Migrations/2024_01_27_Create_Date.ts +++ b/backend/src/Migrations/2024_01_27_Create_Date.ts @@ -2,22 +2,28 @@ import { Kysely } from 'kysely' export async function up(db: Kysely): Promise { await db.schema.alterTable('electionDB') - .addColumn('claim_key_hash','varchar') + .addColumn('claim_key_hash', 'varchar') .addColumn('is_public', 'boolean') .addColumn('create_date', 'varchar') .execute() await db.updateTable('electionDB') - .set({create_date: new Date().toISOString()}) + .set({ create_date: new Date().toISOString() }) .execute() await db.schema.alterTable('electionDB') - .alterColumn('create_date',(col) => col.setNotNull()) + .alterColumn('create_date', (col) => col.setNotNull()) .execute() await db.schema.alterTable('electionRollDB') .dropColumn('ip_address') - .addColumn('ip_hash','varchar') + .addColumn('ip_hash', 'varchar') + .execute() + + + await db.schema.alterTable('ballotDB') + .dropColumn('ip_address') + .addColumn('ip_hash', 'varchar') .execute() } @@ -30,6 +36,12 @@ export async function down(db: Kysely): Promise { await db.schema.alterTable('electionRollDB') .dropColumn('ip_hash') - .addColumn('ip_address','varchar') + .addColumn('ip_address', 'varchar') + .execute() + + + await db.schema.alterTable('ballotDB') + .dropColumn('ip_hash') + .addColumn('ip_address', 'varchar') .execute() - } \ No newline at end of file +} \ No newline at end of file diff --git a/backend/src/Models/CastVoteStore.ts b/backend/src/Models/CastVoteStore.ts index 6da45288..dd16c0e3 100644 --- a/backend/src/Models/CastVoteStore.ts +++ b/backend/src/Models/CastVoteStore.ts @@ -27,14 +27,13 @@ export default class CastVoteStore { ballot.user_id, ballot.status, ballot.date_submitted, - ballot.ip_address, JSON.stringify(ballot.votes), JSON.stringify(ballot.history), ballot.precinct, ]; const ballotSQL = pgFormat( - `INSERT INTO ${this._ballotTableName} (ballot_id,election_id,user_id,status,date_submitted,ip_address,votes,history,precinct) + `INSERT INTO ${this._ballotTableName} (ballot_id,election_id,user_id,status,date_submitted,ip_hash,votes,history,precinct) VALUES (%L);`, ballotValues ); diff --git a/backend/src/Models/ElectionRolls.ts b/backend/src/Models/ElectionRolls.ts index aff336d4..9244633c 100644 --- a/backend/src/Models/ElectionRolls.ts +++ b/backend/src/Models/ElectionRolls.ts @@ -104,7 +104,7 @@ export default class ElectionRollDB implements IElectionRollStore { })) } - getElectionRoll(election_id: string, voter_id: string | null, email: string | null, ip_address: string | null, ctx: ILoggingContext): Promise { + getElectionRoll(election_id: string, voter_id: string | null, email: string | null, ip_hash: string | null, ctx: ILoggingContext): Promise { Logger.debug(ctx, `${tableName}.get election:${election_id}, voter:${voter_id}`); return this._postgresClient @@ -121,8 +121,8 @@ export default class ElectionRollDB implements IElectionRollStore { ors.push(eb.cmpr('email', '=', email)) } - if (ip_address) { - ors.push(eb.cmpr('ip_address', '=', ip_address)) + if (ip_hash) { + ors.push(eb.cmpr('ip_hash', '=', ip_hash)) } return eb.or(ors) diff --git a/backend/src/Models/Elections.ts b/backend/src/Models/Elections.ts index 88d0c5ad..88acf3a0 100644 --- a/backend/src/Models/Elections.ts +++ b/backend/src/Models/Elections.ts @@ -28,7 +28,6 @@ export default class ElectionsDB { createElection(election: Election, ctx: ILoggingContext, reason: string): Promise { Logger.debug(ctx, `${tableName}.createElection`, election); - election.create_date = new Date().toISOString() const newElection = this._postgresClient .insertInto(tableName) .values(election) diff --git a/backend/src/Models/IElectionRollStore.ts b/backend/src/Models/IElectionRollStore.ts index 5e2e7dd3..af2cd731 100644 --- a/backend/src/Models/IElectionRollStore.ts +++ b/backend/src/Models/IElectionRollStore.ts @@ -20,7 +20,7 @@ export interface IElectionRollStore { election_id: string, voter_id: string|null, email: string|null, - ip_address: string|null, + ip_hash: string|null, ctx:ILoggingContext ) => Promise; update: ( diff --git a/backend/src/Models/__mocks__/ElectionRolls.ts b/backend/src/Models/__mocks__/ElectionRolls.ts index f07f7704..53228d3c 100644 --- a/backend/src/Models/__mocks__/ElectionRolls.ts +++ b/backend/src/Models/__mocks__/ElectionRolls.ts @@ -48,11 +48,11 @@ export default class ElectionRollDB implements IElectionRollStore{ return Promise.resolve(res) } - getElectionRoll(election_id: string, voter_id: string|null, email: string|null, ip_address: string|null, ctx:ILoggingContext): Promise<[ElectionRoll] | null> { - Logger.debug(ctx, `MockElectionRolls get election:${election_id}, voter_id:${voter_id}, email: ${email}, ip_address: ${ip_address}`); + getElectionRoll(election_id: string, voter_id: string|null, email: string|null, ip_hash: string|null, ctx:ILoggingContext): Promise<[ElectionRoll] | null> { + Logger.debug(ctx, `MockElectionRolls get election:${election_id}, voter_id:${voter_id}, email: ${email}, ip_hash: ${ip_hash}`); let roll = this._electionRolls.filter(electionRolls => { if (electionRolls.election_id!==election_id) return false - if (ip_address && electionRolls.ip_address===ip_address) return true + if (ip_hash && electionRolls.ip_hash===ip_hash) return true if (voter_id && electionRolls.voter_id ===voter_id) return true if (email && electionRolls.email === email) return true return false diff --git a/domain_model/Ballot.ts b/domain_model/Ballot.ts index e7136614..5d05b030 100644 --- a/domain_model/Ballot.ts +++ b/domain_model/Ballot.ts @@ -10,7 +10,7 @@ export interface Ballot { user_id?: Uid; //ID of user who cast ballot TODO: replace with voter ID status: string; //Status of string (saved, submitted) date_submitted: number; //time ballot was submitted, represented as unix timestamp (Date.now()) - ip_address?: string; // ip address if once_per_ip is enabled + ip_hash?: string; // ip address if once_per_ip is enabled votes: Vote[]; // One per poll history?: BallotAction[]; precinct?: string; // Precint of voter diff --git a/frontend/src/components/Election/Admin/ViewElectionRolls.tsx b/frontend/src/components/Election/Admin/ViewElectionRolls.tsx index 8c0225bf..a12acaa7 100644 --- a/frontend/src/components/Election/Admin/ViewElectionRolls.tsx +++ b/frontend/src/components/Election/Admin/ViewElectionRolls.tsx @@ -138,7 +138,7 @@ const ViewElectionRolls = () => { voter_id: roll.voter_id, email: roll.email || '', invite_status: invite_status, - ip: roll.ip_address || '', + ip: roll.ip_hash || '', precinct: roll.precinct || '', has_voted: roll.submitted.toString(), state: roll.state.toString() From fe2d380ecf557f404612ba433bec67ac947bd1b0 Mon Sep 17 00:00:00 2001 From: mikefranze <41272412+mikefranze@users.noreply.github.com> Date: Sat, 27 Jan 2024 23:27:28 -0800 Subject: [PATCH 8/9] missed a component --- frontend/src/components/ElectionForm/ElectionForm.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/src/components/ElectionForm/ElectionForm.tsx b/frontend/src/components/ElectionForm/ElectionForm.tsx index 23aaaa7a..7aa30de5 100644 --- a/frontend/src/components/ElectionForm/ElectionForm.tsx +++ b/frontend/src/components/ElectionForm/ElectionForm.tsx @@ -172,6 +172,7 @@ const ElectionForm = ({ authSession, onSubmitElection, prevElectionData, submitT state: 'draft', frontend_url: '', owner_id: '', + create_date: '', races: [ { race_id: '0', From 9a0fb846d12fccdc5b2e8c6b82cc5542f46cd69d Mon Sep 17 00:00:00 2001 From: eznarf <41272412+eznarf@users.noreply.github.com> Date: Mon, 29 Jan 2024 09:34:54 -0800 Subject: [PATCH 9/9] Updating from feedback --- backend/src/Controllers/controllerUtils.ts | 2 +- backend/src/Controllers/voterRollUtils.ts | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/backend/src/Controllers/controllerUtils.ts b/backend/src/Controllers/controllerUtils.ts index ac2dd097..198664b3 100644 --- a/backend/src/Controllers/controllerUtils.ts +++ b/backend/src/Controllers/controllerUtils.ts @@ -41,5 +41,5 @@ export function expectPermission(roles:roles[],permission:permission):any { } export function hashString(inputString: string) { - return createHash('sha1').update(inputString).digest('hex') + return createHash('sha256').update(inputString).digest('hex') } \ No newline at end of file diff --git a/backend/src/Controllers/voterRollUtils.ts b/backend/src/Controllers/voterRollUtils.ts index 5c180d8e..1b33f828 100644 --- a/backend/src/Controllers/voterRollUtils.ts +++ b/backend/src/Controllers/voterRollUtils.ts @@ -13,9 +13,9 @@ const ElectionRollModel = ServiceLocator.electionRollDb(); export async function getOrCreateElectionRoll(req: IRequest, election: Election, ctx: ILoggingContext): Promise { // Checks for existing election roll for user Logger.info(req, `getOrCreateElectionRoll`) - + const ip_hash = hashString(req.ip) // Get data that is used for voter authentication - const ip_hash = election.settings.voter_authentication.ip_address ? hashString(req.ip) : null + const require_ip_hash = election.settings.voter_authentication.ip_address ? ip_hash : null const email = election.settings.voter_authentication.email ? req.user?.email : null let voter_id = election.settings.voter_authentication.voter_id ? req.cookies?.voter_id : null @@ -23,8 +23,8 @@ export async function getOrCreateElectionRoll(req: IRequest, election: Election, // This is an odd way of going about this, rather than getting a roll that matches all three we get all that match any of the fields and // check the output for a number of edge cases. var electionRollEntries = null - if ((ip_hash || email || voter_id)) { - electionRollEntries = await ElectionRollModel.getElectionRoll(String(election.election_id), voter_id, email, ip_hash, ctx); + if ((require_ip_hash || email || voter_id)) { + electionRollEntries = await ElectionRollModel.getElectionRoll(String(election.election_id), voter_id, email, require_ip_hash, ctx); } @@ -47,12 +47,12 @@ export async function getOrCreateElectionRoll(req: IRequest, election: Election, election_id: String(election.election_id), email: req.user?.email ? req.user.email : undefined, voter_id: new_voter_id, - ip_hash: hashString(req.ip), + ip_hash: ip_hash, submitted: false, state: ElectionRollState.approved, history: history, }] - if ((ip_hash || email || voter_id)) { + if ((require_ip_hash || email || voter_id)) { const newElectionRoll = await ElectionRollModel.submitElectionRoll(roll, ctx, `User requesting Roll and is authorized`) if (!newElectionRoll) { Logger.error(ctx, "Failed to update ElectionRoll");