diff --git a/backend/package.json b/backend/package.json index 1a209022..c1f89121 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-up.js", + "migrate:down": "node ./build/backend/src/Migrators/migrate-down.js" }, "keywords": [], "author": "", diff --git a/backend/src/Controllers/controllerUtils.ts b/backend/src/Controllers/controllerUtils.ts index e9ab989a..198664b3 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('sha256').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..1b33f828 100644 --- a/backend/src/Controllers/voterRollUtils.ts +++ b/backend/src/Controllers/voterRollUtils.ts @@ -6,15 +6,16 @@ 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(); 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_address = election.settings.voter_authentication.ip_address ? 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 @@ -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 ((require_ip_hash || email || voter_id)) { + electionRollEntries = await ElectionRollModel.getElectionRoll(String(election.election_id), voter_id, email, require_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: ip_hash, submitted: false, state: ElectionRollState.approved, history: history, }] - if ((ip_address || 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"); @@ -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 new file mode 100644 index 00000000..3ef392b8 --- /dev/null +++ b/backend/src/Migrations/2024_01_27_Create_Date.ts @@ -0,0 +1,47 @@ +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: new Date().toISOString() }) + .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() + + + await db.schema.alterTable('ballotDB') + .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() + + + await db.schema.alterTable('ballotDB') + .dropColumn('ip_hash') + .addColumn('ip_address', 'varchar') + .execute() +} \ No newline at end of file diff --git a/backend/src/Migrators/migrate-down.ts b/backend/src/Migrators/migrate-down.ts new file mode 100644 index 00000000..6661edd3 --- /dev/null +++ b/backend/src/Migrators/migrate-down.ts @@ -0,0 +1,11 @@ +require('dotenv').config() +import { createMigrator, handleMigration } from "./migration-utils" + +async function migrateDown() { + const { migrator, db } = createMigrator() + handleMigration(await migrator.migrateDown()) + + await db.destroy() + } + + migrateDown() \ 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..b713a78a --- /dev/null +++ b/backend/src/Migrators/migrate-up.ts @@ -0,0 +1,11 @@ +require('dotenv').config() +import { createMigrator, handleMigration } from "./migration-utils" + +async function migrateUp() { + const { migrator, db } = createMigrator() + handleMigration(await migrator.migrateUp()) + + await db.destroy() + } + + migrateUp() \ 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..6d3f0b93 --- /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/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 3cec2b10..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); - 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/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 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 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/domain_model/Election.ts b/domain_model/Election.ts index 340cf473..177b8538 100644 --- a/domain_model/Election.ts +++ b/domain_model/Election.ts @@ -19,8 +19,14 @@ 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; } +type Omit = Pick> +type PartialBy = Omit & Partial> +export interface NewElection extends PartialBy {} export function electionValidation(obj:Election): string | null { if (!obj){ 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 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() 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/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', 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) => {