diff --git a/client/.env b/client/.env index 02f2863..fea247c 100644 --- a/client/.env +++ b/client/.env @@ -1,2 +1,2 @@ -REACT_APP_SSO_CLIENT_ID=react-boilerplate +REACT_APP_SSO_CLIENT_ID=vote REACT_APP_SSO_AUTHORITY=https://sso.csh.rit.edu/auth/realms/csh \ No newline at end of file diff --git a/client/src/components/App.tsx b/client/src/components/App.tsx index a25abec..8e4e716 100644 --- a/client/src/components/App.tsx +++ b/client/src/components/App.tsx @@ -1,7 +1,7 @@ import React, { Component } from "react"; import { Switch, Route, BrowserRouter as Router } from "react-router-dom"; import { withOidcSecure } from "@axa-fr/react-oidc-context"; -import { Home, Vote, Create } from "./index"; +import { Home, Vote, Create, Result } from "./index"; import PageContainer from "../containers/PageContainer"; class App extends Component { @@ -17,6 +17,11 @@ class App extends Component { component={withOidcSecure(Vote)} /> + diff --git a/client/src/components/NavBar/NavBar.tsx b/client/src/components/NavBar/NavBar.tsx index 6a8ca3b..89c7627 100644 --- a/client/src/components/NavBar/NavBar.tsx +++ b/client/src/components/NavBar/NavBar.tsx @@ -9,6 +9,7 @@ import { } from "reactstrap"; import { NavLink } from "react-router-dom"; import Profile from "./Profile"; +import { useReactOidc } from "@axa-fr/react-oidc-context"; const NavBar: React.FunctionComponent = () => { const [isOpen, setIsOpen] = React.useState(false); @@ -16,8 +17,13 @@ const NavBar: React.FunctionComponent = () => { const toggle = () => { setIsOpen(!isOpen); }; + const { oidcUser } = useReactOidc(); + let evals = false; + if (oidcUser) { + evals = oidcUser.profile.groups.includes("eboard-evaluations"); + + } - const isEvals = true; return (
@@ -34,7 +40,7 @@ const NavBar: React.FunctionComponent = () => { Home - {isEvals ? + {evals ? Create diff --git a/client/src/components/Pages/Create/index.tsx b/client/src/components/Pages/Create/index.tsx index 84098aa..04aa511 100644 --- a/client/src/components/Pages/Create/index.tsx +++ b/client/src/components/Pages/Create/index.tsx @@ -2,6 +2,7 @@ import React, { useState } from "react"; import { useHistory } from 'react-router-dom'; import Spinner from "../../Spinner"; import "./create.css"; +import { useReactOidc } from "@axa-fr/react-oidc-context"; export const Create: React.FunctionComponent = () =>{ const [pollType, setPollType] = useState("PassFail"); @@ -9,6 +10,9 @@ export const Create: React.FunctionComponent = () =>{ const [pollOptions, setPollOptions] = useState(["Pass", "Fail or Conditional", "Abstain"]); const [loading, setLoading] = useState(false); const [error, setError] = useState(false); + const { oidcUser } = useReactOidc(); + const evals = oidcUser.profile.groups.includes("eboard-evaluations"); + let history = useHistory(); function handleSelectPollType(changeEvent:React.FormEvent) { @@ -52,10 +56,14 @@ export const Create: React.FunctionComponent = () =>{ setLoading(true); const body = { "title": pollTitle, - "options": pollOptions + "options": pollOptions, + "type": pollType } fetch("http://localhost:5000/api/initializePoll", { - headers: {"content-type": "application/json"}, + headers: new Headers({ + 'Authorization': 'Bearer ' + oidcUser.access_token, + "content-type": "application/json" + }), method: "POST", body: JSON.stringify(body) }) @@ -73,13 +81,12 @@ export const Create: React.FunctionComponent = () =>{ .catch((error) => { setLoading(false); setError(true); - console.log(error); }); } return( loading ? : - error ?
Something went wrong : (
: + error || !evals ?
Something went wrong : (
:
Create Vote
diff --git a/client/src/components/Pages/Home/index.test.tsx b/client/src/components/Pages/Home/index.test.tsx deleted file mode 100644 index 5fcee4e..0000000 --- a/client/src/components/Pages/Home/index.test.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import React from "react"; -import { render } from "@testing-library/react"; -import Home from "."; - -test("renders main app page", () => { - const { getByTestId } = render(); - const divElement = getByTestId('spinner'); - expect(divElement).toBeDefined(); -}); diff --git a/client/src/components/Pages/Home/index.tsx b/client/src/components/Pages/Home/index.tsx index e0032ed..ee6606e 100644 --- a/client/src/components/Pages/Home/index.tsx +++ b/client/src/components/Pages/Home/index.tsx @@ -1,51 +1,38 @@ -import React, {Component} from "react"; +import React, {useEffect, useState} from "react"; import Spinner from "../../Spinner"; import PollList from "../../PollList"; +import { useReactOidc } from "@axa-fr/react-oidc-context"; -type HomeProps = {}; -type HomeState = { - error: null | string, - isLoaded: boolean, - currentPolls: Array -} -class Home extends Component{ - constructor(props:HomeProps) { - super(props); - this.state = { - error: null, - isLoaded: false, - currentPolls: [] - }; - } - componentDidMount() { - fetch("http://localhost:5000/api/getCurrentPolls") +export const Home: React.FunctionComponent = () =>{ + const [error, setError] = useState(null); + const [isLoaded, setLoaded] = useState(false); + const [currentPolls, setCurrentPolls] = useState([]); + const { oidcUser } = useReactOidc(); + + useEffect(() => { + + fetch("http://localhost:5000/api/getCurrentPolls", { headers: new Headers({ + 'Authorization': 'Bearer ' + oidcUser.access_token + })}) .then(res => res.json()) .then((result) => { - this.setState({ - isLoaded: true, - currentPolls: result - }); + setLoaded(true); + setCurrentPolls(result); }, (error) => { - this.setState({ - isLoaded: true, - error - }); + setLoaded(true); + setError(error); }); - } - render() { - const {error, isLoaded, currentPolls} = this.state; - if (error) { - return( + }, [oidcUser.access_token ]) + + return ( + error ?
Something went wrong! Please Try again
- ); - } else if(!isLoaded) { - return() - }else { - return( - - )} - } + : !isLoaded ? + + : + ) + } export default Home; diff --git a/client/src/components/Pages/Result/index.tsx b/client/src/components/Pages/Result/index.tsx new file mode 100644 index 0000000..85ef60d --- /dev/null +++ b/client/src/components/Pages/Result/index.tsx @@ -0,0 +1,150 @@ +import React, { useEffect, useState } from "react"; +import { useParams, useHistory } from 'react-router-dom'; +import Spinner from "../../Spinner"; +import "./result.css"; +import { useReactOidc } from "@axa-fr/react-oidc-context"; + +type RouteParams = { + voteId: string +} + +type Poll = { + name: string, + results: Object +} + +export const Result: React.FunctionComponent = () =>{ + const { voteId } = useParams(); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(false); + + const [poll, setPoll] = useState(undefined); + const [ended, setEnded] = useState(false); + let history = useHistory(); + const { oidcUser } = useReactOidc(); + const evals = oidcUser.profile.groups.includes("eboard-evaluations"); + useEffect(() => { + const interval = setInterval(() => { + if(!ended) { + fetch("http://localhost:5000/api/getCount", { + headers: new Headers({ + 'Authorization': 'Bearer ' + oidcUser.access_token, + "content-type": "application/json" + }), + method: "POST", + body: JSON.stringify({"voteId": voteId}) + }) + .then((res) => { + switch(res.status) { + case 200: + return res.json() + case 404: + return undefined + default: + throw new Error("Error Getting Poll Details"); + } + }) + .then((result) => { + setLoading(false); + setPoll(result) + }, + (error) => { + setLoading(false); + setError(true); + }); + } + }, 10000); + return () => clearInterval(interval); + }, [voteId, ended, oidcUser.access_token]); + + useEffect(() => { + fetch("http://localhost:5000/api/getCount", { + headers: new Headers({ + 'Authorization': 'Bearer ' + oidcUser.access_token, + "content-type": "application/json" + }), + method: "POST", + body: JSON.stringify({"voteId": voteId}) + }) + .then((res) => { + switch(res.status) { + case 200: + return res.json() + case 404: + return undefined + default: + throw new Error("Error Getting Poll Details"); + } + }) + .then((result) => { + setLoading(false); + setPoll(result) + }, + (error) => { + setLoading(false); + setError(true); + }); + }, [voteId, oidcUser.access_token]) + + function endVoting() { + if(window.confirm("End Voting?")){ + setLoading(true) + fetch("http://localhost:5000/api/endPoll", { + headers: new Headers({ + 'Authorization': 'Bearer ' + oidcUser.access_token, + "content-type": "application/json" + }), + method: "POST", + body: JSON.stringify({"voteId": voteId}) + }) + .then((res) => { + switch(res.status) { + case 200: + return res.json() + case 404: + return undefined + default: + throw new Error("Error Getting Poll Details"); + } + }) + .then((result) => { + setLoading(false); + setPoll(result) + setEnded(true) + }, + (error) => { + setLoading(false); + setError(true); + }); + } + } + + return( + loading ? : + error ?
Something went wrong : (
: + poll === undefined ? +
Poll not found : (
: +
+
+
{poll.name} {ended ? "Final" : null} Results
+
+ {ended? null :
These are refreshed every 10 seconds
} +
+ {Object.entries(poll.results).map(function(option, idx){ + return (
  • {option[0]} : {option[1]}
  • ) + })} +
    +
    +
    +
    + {evals && !ended ? : } +
    + +
    + ) +} + +export default Result; diff --git a/client/src/components/Pages/Result/result.css b/client/src/components/Pages/Result/result.css new file mode 100644 index 0000000..cf779e1 --- /dev/null +++ b/client/src/components/Pages/Result/result.css @@ -0,0 +1,32 @@ +.result-title-panel { + color: inherit; + font-size: 20px; + padding: 20px; +} + +.result-list { + border: none; + border-radius: 2px; + box-shadow: 0 1px 4px rgba(0,0,0,.3); +} + +.refresh-message { + padding: 12px; +} + +.result-list-items{ + background-color: white; + font-size: 14px; + padding:12px; + padding-right: 16px; +} + +.result-list-items li { + list-style: none; + padding-top: 12px; +} + +.exit-button { + margin-top: 24px; + text-align: center; +} diff --git a/client/src/components/Pages/Vote/index.tsx b/client/src/components/Pages/Vote/index.tsx index d45a197..bfb3fb1 100644 --- a/client/src/components/Pages/Vote/index.tsx +++ b/client/src/components/Pages/Vote/index.tsx @@ -2,13 +2,15 @@ import React, { useEffect, useState } from "react"; import { useParams, useHistory } from 'react-router-dom'; import Spinner from "../../Spinner"; import "./vote.css"; +import { useReactOidc } from "@axa-fr/react-oidc-context"; + type RouteParams = { voteId: string } type Poll = { - id: string, - name: string, - voteOptions: Array, + _id: string, + title: string, + choices: Array, } export const Vote: React.FunctionComponent = () =>{ @@ -18,12 +20,16 @@ export const Vote: React.FunctionComponent = () =>{ const [error, setError] = useState(false); const [selected, setSelected] = useState(null); + const { oidcUser } = useReactOidc(); let history = useHistory(); useEffect(() => { fetch("http://localhost:5000/api/getPollDetails", { - headers: {"content-type": "application/json"}, + headers: new Headers({ + 'Authorization': 'Bearer ' + oidcUser.access_token, + "content-type": "application/json" + }), method: "POST", body: JSON.stringify({"voteId": voteId}) }) @@ -44,32 +50,31 @@ export const Vote: React.FunctionComponent = () =>{ (error) => { setLoading(false); setError(true); - console.log(error); }); - }, [voteId]) + }, [voteId, oidcUser.access_token]) function buttonClick(idx:number|null) { if (idx !== null) { fetch("http://localhost:5000/api/sendVote", { - headers: {"content-type": "application/json"}, + headers: new Headers({ + 'Authorization': 'Bearer ' + oidcUser.access_token, + "content-type": "application/json" + }), method: "POST", - body: JSON.stringify({"voteId": voteId, "voteChoice": idx}) + body: JSON.stringify({"voteId": voteId, "voteChoice": poll?.choices[idx]}) }) .then((res) => { switch(res.status) { case 204: - //TODO- probably better to route to a "voted" screen than home - //but this is prob okay for mvp - history.push("/") + history.push("/result/" + voteId) return; default: throw new Error("Error Voting"); } }) .catch((error) => { - setLoading(false); - setError(true); - console.log(error); + window.alert("An error occurred (You may have already voted)") + history.push("/result/" + voteId) }); } } @@ -80,16 +85,16 @@ export const Vote: React.FunctionComponent = () =>{
    Poll not found : (
    :
    -
    {poll.name}
    +
    {poll.title}
    - {poll.voteOptions.map(function(option, idx){ + {poll.choices.map(function(option, idx){ return (
  • ) })}
    -
    You have selected: { selected !== null ? poll.voteOptions[selected] : null}
    +
    You have selected: { selected !== null ? poll.choices[selected] : null}

    diff --git a/client/src/components/index.ts b/client/src/components/index.ts index 2613c3d..a57b7c1 100644 --- a/client/src/components/index.ts +++ b/client/src/components/index.ts @@ -2,3 +2,4 @@ export { default as Home } from "./Pages/Home"; export { default as NavBar } from "./NavBar"; export { default as Vote } from "./Pages/Vote"; export { default as Create } from "./Pages/Create"; +export { default as Result } from "./Pages/Result"; diff --git a/client/src/config.ts b/client/src/config.ts index 3c77416..169c694 100644 --- a/client/src/config.ts +++ b/client/src/config.ts @@ -5,7 +5,7 @@ const configuration = { }/authentication/callback`, response_type: "code", post_logout_redirect_uri: "http://localhost:3000/", - scope: "openid profile email offline_access", + scope: "openid profile email offline_access groups", authority: process.env.REACT_APP_SSO_AUTHORITY, silent_redirect_uri: `${window.location.protocol}//${ window.location.hostname diff --git a/package.json b/package.json index 4b73b0e..8096515 100644 --- a/package.json +++ b/package.json @@ -7,19 +7,28 @@ "@testing-library/jest-dom": "^4.2.4", "@testing-library/react": "^9.3.2", "@testing-library/user-event": "^7.1.2", + "@types/express-session": "^1.17.0", "body-parser": "^1.19.0", + "connect-ensure-login": "^0.1.1", "cors": "^2.8.5", "csh-material-bootstrap": "^4.5.2", "express": "^4.17.1", + "express-session": "^1.17.1", "history": "^5.0.0", + "mongodb": "^3.6.2", + "https": "^1.0.0", "oidc-client": "^1.10.1", + "passport": "^0.4.1", + "passport-jwt": "^4.0.0", + "passport-openidconnect": "0.0.2", "react": "^16.13.1", "react-dom": "^16.13.1", "react-router": "^5.2.0", "react-router-dom": "^5.2.0", "react-scripts": "^3.4.3", "reactstrap": "^8.6.0", - "ts-node": "^9.0.0" + "ts-node": "^9.0.0", + "url": "^0.11.0" }, "scripts": { "start": "concurrently \"ts-node ./service/index.ts\" \"cd client && react-scripts start\"", diff --git a/service/index.ts b/service/index.ts index 36c140e..b2ae7a6 100644 --- a/service/index.ts +++ b/service/index.ts @@ -1,63 +1,283 @@ +//@ts-nocheck + import * as express from "express"; import * as path from "path"; import * as cors from "cors"; +import * as dotenv from 'dotenv'; + +var MongoClient = require('mongodb').MongoClient + + +var ObjectID = require('mongodb').ObjectID; + +dotenv.config(); + +let db = null; + +MongoClient.connect(process.env.DB_URL, function (err, client) { + if (err) throw(err) + db = client.db("vote") +}) + +const passport = require('passport'); + +const https = require('https') +var url = require("url"); + +var JwtStrategy = require('passport-jwt').Strategy, + ExtractJwt = require('passport-jwt').ExtractJwt; + + +const options = { + hostname: 'sso.csh.rit.edu', + path: '/auth/realms/csh/.well-known/openid-configuration' +} + + +https.get(options, res => { + res.setEncoding('utf8'); + res.on('data', function (chunk) { + const jwks_uri = JSON.parse(chunk).jwks_uri; + https.get({hostname: url.parse(jwks_uri).hostname, path: url.parse(jwks_uri).pathname}, res => { + res.setEncoding('utf8'); + res.on('data', function (chunk) { + let secretOrKey = JSON.parse(chunk).keys[0].x5c[0] + secretOrKey = secretOrKey.match(/.{1,64}/g).join('\n'); + secretOrKey = `-----BEGIN CERTIFICATE-----\n${secretOrKey}\n-----END CERTIFICATE-----\n`; + var opts = { + jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), + secretOrKey: secretOrKey, + issuer: 'https://sso.csh.rit.edu/auth/realms/csh' + } + passport.use(new JwtStrategy(opts, function(jwt_payload, done) { + return done(null, jwt_payload); + })); + + }) + }) + }); +}) + +// Configure Passport authenticated session persistence. +passport.serializeUser((user, cb) => { + cb(null, user); +}); + +passport.deserializeUser((obj, cb) => { + cb(null, obj); +}); const app = express(); -app.use(cors()); +app.use(passport.initialize()); + + app.use(express.json()); +app.use(express.static(path.join(__dirname, "/../build"))); +app.use(cors()); -let currentPolls = [{"id": 1, "name": "eat a whole cake conditional", "voteOptions": ["eat 1 cake","eat 2 cakes", "eat no cakes : ("]}, -{"id": 2, "name": "fail chad", "voteOptions": ["fail", "conditional", "abstain"]} +let currentPolls = [ + { title: 'test Poll', + choices: ["fail", "conditional", "abstain"], + type: 'FailConditional', + _id: "5f9b2d5d601e1c6971430638" } ]; app.use(express.static(path.join(__dirname, "/../build"))); // Returns list of all current polls -app.get("/api/getCurrentPolls", (req,res) => { - res.json(currentPolls); +app.get("/api/getCurrentPolls", passport.authenticate('jwt'), (req,res) => { + https.get({hostname: "sso.csh.rit.edu", + path: "/auth/realms/csh/protocol/openid-connect/userinfo", + headers: {"Authorization": req.headers.authorization} + }, infoRes => { + infoRes.setEncoding('utf8'); + infoRes.on('data', function (chunk) { + const groups = JSON.parse(chunk).groups; + if (!groups.includes("active")) { + res.status(403).send(); + } else if (groups.includes("10weeks") || groups.includes("fall_coop")) { + res.status(403).send(); + } else { + res.json(currentPolls) + } + }); + }); }); -app.post("/api/getPollDetails", (req,res) => { - const poll = currentPolls.find(x => x.id === parseInt(req.body.voteId)); - if (poll) { - res.json(poll) - } else { - res.status(404).send(); - } +app.post("/api/getPollDetails", passport.authenticate('jwt'), (req,res) => { + const poll = currentPolls.find(x => x._id === req.body.voteId); + https.get({hostname: "sso.csh.rit.edu", + path: "/auth/realms/csh/protocol/openid-connect/userinfo", + headers: {"Authorization": req.headers.authorization} + }, infoRes => { + infoRes.setEncoding('utf8'); + infoRes.on('data', function (chunk) { + const groups = JSON.parse(chunk).groups; + if (!groups.includes("active")) { + res.status(403).send(); + } else if (groups.includes("10weeks") || groups.includes("fall_coop")) { + res.status(403).send(); + } else { + if (poll) { + res.json(poll) + } else { + res.status(404).send(); + } + } + }); + }); }); // Send in a client's vote, called from any voting screen -app.post("/api/sendVote", (req,res) => { - res.status(204).send(); +app.post("/api/sendVote", passport.authenticate('jwt'), (req,res) => { + https.get({hostname: "sso.csh.rit.edu", + path: "/auth/realms/csh/protocol/openid-connect/userinfo", + headers: {"Authorization": req.headers.authorization} + }, infoRes => { + infoRes.setEncoding('utf8'); + infoRes.on('data', function (chunk) { + const groups = JSON.parse(chunk).groups; + if (!groups.includes("active")) { + res.status(403).send(); + } else if (groups.includes("10weeks") || groups.includes("fall_coop")) { + res.status(403).send(); + } else { + const vote = { + time: new Date(), + userName: JSON.parse(chunk).preferred_username, + choice: req.body.voteChoice, + poll: ObjectID.createFromHexString(req.body.voteId) + } + const findData = { + userName: vote.userName, + poll: vote.poll + } + const collection = db.collection("Votes"); + + collection.findOne(findData).then(hasVoted => { + if(hasVoted) { + res.status(400).send(); + } else { + collection.insert(vote, function(err,docsInserted){ + res.status(204).send(); + }); + } + }) + } + }); + }); }); // Initialize a poll/vote, called from eval's init screen -app.post("/api/initializePoll", (req,res) => { - // TODO - the id should actually be generated by putting it in the db - // for now it's just the idx in the list since we're not doing db things yet - const newPoll = { - "id": currentPolls.length + 1, - "name": req.body.title, - "voteOptions": req.body.options - } - currentPolls.push(newPoll); - res.json({"pollId": newPoll.id}); +app.post("/api/initializePoll", passport.authenticate('jwt'), (req,res) => { + https.get({hostname: "sso.csh.rit.edu", + path: "/auth/realms/csh/protocol/openid-connect/userinfo", + headers: {"Authorization": req.headers.authorization} + }, infoRes => { + infoRes.setEncoding('utf8'); + infoRes.on('data', function (chunk) { + const groups = JSON.parse(chunk).groups; + if (!groups.includes("eboard-evaluations")) { + res.status(403).send(); + } else { + const newPoll = { + "_id": null, + "title": req.body.title, + "choices": req.body.options, + "type": req.body.type, + "time": new Date() + } + const collection = db.collection("Polls") + collection.insert(newPoll, function(err,docsInserted){ + newPoll._id = newPoll._id.toHexString(); + currentPolls.push(newPoll); + res.json({"pollId": newPoll._id}); + }); + } + }); + }); }); // get the count without ending the poll -app.get("/api/getCount", (req,res) => { - res.json({"countYes": 1, "countNo": 2, "countAbstain": 0, "totalCount": 3}); +app.post("/api/getCount",passport.authenticate('jwt'), (req,res) => { + https.get({hostname: "sso.csh.rit.edu", + path: "/auth/realms/csh/protocol/openid-connect/userinfo", + headers: {"Authorization": req.headers.authorization} + }, infoRes => { + infoRes.setEncoding('utf8'); + infoRes.on('data', function (chunk) { + const groups = JSON.parse(chunk).groups; + if (!groups.includes("active")) { + res.status(403).send(); + } else if (groups.includes("10weeks") || groups.includes("fall_coop")) { + res.status(403).send(); + } else { + const count = {name: "", results:{}} + const pollCollection = db.collection("Polls") + pollCollection.findOne({"_id": ObjectID.createFromHexString(req.body.voteId)}).then(poll => { + count.name = poll.title; + }); + const collection = db.collection("Votes") + collection.find({"poll": ObjectID.createFromHexString(req.body.voteId)}).toArray(function(err, result) { + if (err) throw err; + result.forEach(element => { + const choice = element.choice; + if (count.results[choice]) { + count.results[choice] += 1; + } else { + count.results[choice] = 1; + } + }); + res.json(count); + }); + } + }); + }); + + }); // end the poll and get the final results, called from evals view -app.get("/api/endPoll", (req,res) => { - res.json({"countYes": 1, "countNo": 2, "countAbstain": 0, "totalCount": 3}); -}); -/** - * TODO - what other endpoints will we need? - * Will Evals need to get a list of all who voted (to poke people if they're slow)? - */ +app.post("/api/endPoll", passport.authenticate('jwt'), (req,res) => { + https.get({hostname: "sso.csh.rit.edu", + path: "/auth/realms/csh/protocol/openid-connect/userinfo", + headers: {"Authorization": req.headers.authorization} + }, infoRes => { + infoRes.setEncoding('utf8'); + infoRes.on('data', function (chunk) { + const groups = JSON.parse(chunk).groups; + if (!groups.includes("eboard-evaluations")) { + res.status(403).send(); + } else { + let removeIndex = currentPolls.map(item => item._id) + .indexOf(req.body.voteId); + ~removeIndex && currentPolls.splice(removeIndex, 1); + + const count = {name: "", results:{}} + const pollCollection = db.collection("Polls") + pollCollection.findOne({"_id": ObjectID.createFromHexString(req.body.voteId)}).then(poll => { + count.name = poll.title; + }); + const collection = db.collection("Votes") + collection.find({"poll": ObjectID.createFromHexString(req.body.voteId)}).toArray(function(err, result) { + if (err) throw err; + result.forEach(element => { + const choice = element.choice; + if (count.results[choice]) { + count.results[choice] += 1; + } else { + count.results[choice] = 1; + } + }); + res.json(count); + }); + } + }); + }); + + }); + app.get('*', (req,res) =>{ res.sendFile(path.join(__dirname+"/../build/index.html")); });