├── .gitignore
├── backend
├── .gitignore
├── migrations
│ ├── 2_election_migration.js
│ └── 1_initial_migration.js
├── src
│ ├── controllers
│ │ ├── auth
│ │ │ ├── logout.ts
│ │ │ ├── signup.ts
│ │ │ ├── check.ts
│ │ │ └── login.ts
│ │ ├── polls
│ │ │ ├── status.ts
│ │ │ ├── reset.ts
│ │ │ ├── end.ts
│ │ │ ├── votes.ts
│ │ │ ├── fetch.ts
│ │ │ ├── start.ts
│ │ │ └── vote.ts
│ │ └── users
│ │ │ ├── not-verified.ts
│ │ │ ├── delete.ts
│ │ │ └── verify.ts
│ ├── index.ts
│ ├── entity
│ │ ├── Candidate.ts
│ │ ├── Poll.ts
│ │ └── User.ts
│ ├── web3.ts
│ ├── routers
│ │ ├── users.ts
│ │ ├── auth.ts
│ │ └── polls.ts
│ ├── migration
│ │ ├── 1645187823173-users.ts
│ │ ├── 1645175354681-user-update.ts
│ │ └── 1645167790939-initial.ts
│ ├── middlewares
│ │ └── cache.ts
│ ├── server.ts
│ └── seed
│ │ └── users.ts
├── ormconfig.json
├── contracts
│ ├── Migrations.sol
│ └── Election.sol
├── Readme.md
├── package.json
├── truffle-config.js
└── tsconfig.json
├── frontend
├── src
│ ├── react-app-env.d.ts
│ ├── styles
│ │ ├── pages
│ │ │ ├── Admin
│ │ │ │ ├── Polls.scss
│ │ │ │ └── Create.scss
│ │ │ ├── View.scss
│ │ │ ├── Features.scss
│ │ │ ├── User
│ │ │ │ └── Profile.scss
│ │ │ └── Landing.scss
│ │ ├── components
│ │ │ ├── Back.scss
│ │ │ ├── Features
│ │ │ │ └── Feature.scss
│ │ │ ├── Polls
│ │ │ │ ├── Panel.scss
│ │ │ │ └── Chart.scss
│ │ │ └── Waiting.scss
│ │ ├── layouts
│ │ │ ├── Users.scss
│ │ │ ├── Polls.scss
│ │ │ ├── Login.scss
│ │ │ ├── Boxes.scss
│ │ │ └── Default.scss
│ │ ├── vote-status.scss
│ │ ├── form.scss
│ │ └── index.scss
│ ├── axios.ts
│ ├── pages
│ │ ├── Start.tsx
│ │ ├── Home.tsx
│ │ ├── Result.tsx
│ │ ├── Polls.tsx
│ │ ├── Admin
│ │ │ ├── Verify.tsx
│ │ │ ├── Home.tsx
│ │ │ ├── Polls.tsx
│ │ │ ├── Result.tsx
│ │ │ ├── Users.tsx
│ │ │ └── Start.tsx
│ │ ├── User
│ │ │ ├── Profile.tsx
│ │ │ └── Polls.tsx
│ │ ├── View.tsx
│ │ ├── Login.tsx
│ │ └── Signup.tsx
│ ├── components
│ │ ├── Polls
│ │ │ ├── Running.tsx
│ │ │ ├── Finished.tsx
│ │ │ ├── Panel.tsx
│ │ │ └── Chart.tsx
│ │ ├── Footer.tsx
│ │ ├── Waiting.tsx
│ │ ├── Back.tsx
│ │ ├── Home
│ │ │ ├── Landing.tsx
│ │ │ └── Features.tsx
│ │ ├── Features
│ │ │ └── Feature.tsx
│ │ ├── Navbar.tsx
│ │ └── CustomRoutes.tsx
│ ├── setupTests.ts
│ ├── App.tsx
│ ├── reportWebVitals.ts
│ ├── index.tsx
│ ├── layouts
│ │ ├── Login.tsx
│ │ └── Default.tsx
│ └── contexts
│ │ └── Auth.tsx
├── public
│ ├── robots.txt
│ ├── logo.png
│ ├── mobile.png
│ ├── vote.gif
│ ├── favicon.ico
│ ├── logo192.png
│ ├── logo512.png
│ ├── logo-green.png
│ ├── manifest.json
│ └── index.html
├── .gitignore
├── tsconfig.json
├── package.json
└── README.md
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | build
2 | node_modules
3 | node_modules/**/*
4 | .env
--------------------------------------------------------------------------------
/backend/.gitignore:
--------------------------------------------------------------------------------
1 | build
2 | node_modules
3 | node_modules/**/*
4 | .env
--------------------------------------------------------------------------------
/frontend/src/react-app-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/frontend/src/styles/pages/Admin/Polls.scss:
--------------------------------------------------------------------------------
1 | .end-election-button {
2 | margin-top: 50px;
3 | }
4 |
--------------------------------------------------------------------------------
/frontend/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/frontend/public/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/romanshilpakar/Blockchain-Based-Voting-System/HEAD/frontend/public/logo.png
--------------------------------------------------------------------------------
/frontend/public/mobile.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/romanshilpakar/Blockchain-Based-Voting-System/HEAD/frontend/public/mobile.png
--------------------------------------------------------------------------------
/frontend/public/vote.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/romanshilpakar/Blockchain-Based-Voting-System/HEAD/frontend/public/vote.gif
--------------------------------------------------------------------------------
/frontend/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/romanshilpakar/Blockchain-Based-Voting-System/HEAD/frontend/public/favicon.ico
--------------------------------------------------------------------------------
/frontend/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/romanshilpakar/Blockchain-Based-Voting-System/HEAD/frontend/public/logo192.png
--------------------------------------------------------------------------------
/frontend/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/romanshilpakar/Blockchain-Based-Voting-System/HEAD/frontend/public/logo512.png
--------------------------------------------------------------------------------
/frontend/public/logo-green.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/romanshilpakar/Blockchain-Based-Voting-System/HEAD/frontend/public/logo-green.png
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Blockchain-Based-Voting-System
2 |
3 | # Watch the demo how it works here:
4 | https://www.youtube.com/watch?v=f22rJ1m7JBs&ab_channel=ROOMYAN
5 |
--------------------------------------------------------------------------------
/frontend/src/axios.ts:
--------------------------------------------------------------------------------
1 | import axios from "axios";
2 |
3 | export default axios.create({
4 | baseURL: "http://localhost:8000",
5 | withCredentials: true,
6 | });
7 |
--------------------------------------------------------------------------------
/frontend/src/styles/pages/View.scss:
--------------------------------------------------------------------------------
1 | .view-container {
2 | padding: 100px 100px 0 100px;
3 | margin-left: 50px;
4 | margin-right: 50px;
5 | position: relative;
6 | }
7 |
--------------------------------------------------------------------------------
/backend/migrations/2_election_migration.js:
--------------------------------------------------------------------------------
1 | const Election = artifacts.require("Election");
2 |
3 | module.exports = function (deployer) {
4 | deployer.deploy(Election);
5 | };
6 |
--------------------------------------------------------------------------------
/backend/migrations/1_initial_migration.js:
--------------------------------------------------------------------------------
1 | const Migrations = artifacts.require("Migrations");
2 |
3 | module.exports = function (deployer) {
4 | deployer.deploy(Migrations);
5 | };
6 |
--------------------------------------------------------------------------------
/frontend/src/pages/Start.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import Waiting from "../components/Waiting";
3 |
4 | const Start = () => {
5 | return ;
6 | };
7 |
8 | export default Start;
9 |
--------------------------------------------------------------------------------
/backend/src/controllers/auth/logout.ts:
--------------------------------------------------------------------------------
1 | import { Request, Response } from "express";
2 |
3 | export default (req: Request, res: Response) => {
4 | res.clearCookie("refreshToken");
5 | res.end();
6 | };
7 |
--------------------------------------------------------------------------------
/frontend/src/styles/components/Back.scss:
--------------------------------------------------------------------------------
1 | .back {
2 | position: absolute;
3 | left: 0px;
4 | top: 50px;
5 | cursor: pointer;
6 | display: flex;
7 | align-items: center;
8 |
9 | // .icon {
10 | // }
11 | }
12 |
--------------------------------------------------------------------------------
/frontend/src/styles/layouts/Users.scss:
--------------------------------------------------------------------------------
1 | .users-wrapper {
2 | @include boxes-wrapper;
3 |
4 | .user-wrapper {
5 | @include box-wrapper;
6 | display: flex;
7 | justify-content: space-between;
8 | align-items: center;
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/frontend/src/components/Polls/Running.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | const Running = () => {
4 | return (
5 |
8 | );
9 | };
10 |
11 | export default Running;
12 |
--------------------------------------------------------------------------------
/frontend/src/setupTests.ts:
--------------------------------------------------------------------------------
1 | // jest-dom adds custom jest matchers for asserting on DOM nodes.
2 | // allows you to do things like:
3 | // expect(element).toHaveTextContent(/react/i)
4 | // learn more: https://github.com/testing-library/jest-dom
5 | import '@testing-library/jest-dom';
6 |
--------------------------------------------------------------------------------
/frontend/src/styles/layouts/Polls.scss:
--------------------------------------------------------------------------------
1 | .polls-wrapper {
2 | @include boxes-wrapper;
3 |
4 | .poll-wrapper {
5 | @include box-wrapper;
6 |
7 | .candidates-wrapper {
8 | @include inner-box-wrapper;
9 |
10 | .candidate-name {
11 | @include inner-box-name;
12 | }
13 | }
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/frontend/src/styles/components/Features/Feature.scss:
--------------------------------------------------------------------------------
1 | .feature-container {
2 | display: flex;
3 | align-items: center;
4 |
5 | .align-left {
6 | text-align: left;
7 | }
8 | .align-right {
9 | text-align: right;
10 | }
11 |
12 | .icon-container {
13 | font-size: 4rem;
14 | padding: 20px;
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/frontend/src/styles/components/Polls/Panel.scss:
--------------------------------------------------------------------------------
1 | .polls-container {
2 | @include box-shadow;
3 | margin-top: 50px;
4 | text-align: center;
5 | padding: 50px;
6 | border-radius: 10px;
7 | display: flex;
8 | flex-direction: column;
9 | position: relative;
10 |
11 | .votes-wrapper {
12 | padding-top: 50px;
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/backend/src/controllers/polls/status.ts:
--------------------------------------------------------------------------------
1 | import { Request, Response } from "express";
2 | import ElectionContract from "../../web3";
3 |
4 | export default async (_: Request, res: Response) => {
5 | const instance = await ElectionContract.deployed();
6 |
7 | const status = await instance.getStatus();
8 |
9 | return res.send({ status });
10 | };
11 |
--------------------------------------------------------------------------------
/backend/src/index.ts:
--------------------------------------------------------------------------------
1 | import { createConnection } from "typeorm";
2 | import app from "./server";
3 | import "dotenv/config";
4 |
5 | const port = process.env.PORT || 8000;
6 |
7 | createConnection()
8 | .then(async (connection) => {
9 | app.listen(port, () => console.log(`listening on port ${port} ... `));
10 | })
11 | .catch((error) => console.log(error));
12 |
--------------------------------------------------------------------------------
/frontend/src/components/Footer.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | const Footer = () => {
4 | return (
5 |
11 | );
12 | };
13 |
14 | export default Footer;
15 |
--------------------------------------------------------------------------------
/backend/src/controllers/users/not-verified.ts:
--------------------------------------------------------------------------------
1 | import { Request, Response } from "express";
2 | import { User } from "../../entity/User";
3 |
4 | export default async (req: Request, res: Response) => {
5 | const users = await User.find({
6 | select: ["id", "name", "citizenshipNumber", "email"],
7 | where: { verified: false },
8 | });
9 |
10 | return res.send({ users });
11 | };
12 |
--------------------------------------------------------------------------------
/frontend/src/components/Polls/Finished.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | const Finished = () => {
4 | return (
5 |
6 |
7 |
8 | Finished
9 |
10 |
11 | );
12 | };
13 |
14 | export default Finished;
15 |
--------------------------------------------------------------------------------
/frontend/src/components/Waiting.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | const Waiting = () => {
4 | return (
5 |
6 |
7 |
8 |
9 |
10 |
WAITING FOR THE ELECTION TO START
11 |
12 | );
13 | };
14 |
15 | export default Waiting;
16 |
--------------------------------------------------------------------------------
/frontend/src/pages/Home.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { RouteProps } from "react-router";
3 | import Features from "../components/Home/Features";
4 | import Landing from "../components/Home/Landing";
5 |
6 | const Home = (props: RouteProps): JSX.Element => {
7 | return (
8 | <>
9 |
10 |
11 | >
12 | );
13 | };
14 |
15 | export default Home;
16 |
--------------------------------------------------------------------------------
/frontend/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
--------------------------------------------------------------------------------
/backend/ormconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "type": "mysql",
3 | "host": "localhost",
4 | "port": 3306,
5 | "username": "bbvs",
6 | "password": "Password$$",
7 | "database": "bbvs",
8 | "synchronize": false,
9 | "logging": true,
10 | "cli": {
11 | "migrationsDir": "src/migration"
12 | },
13 | "entities": ["src/entity/**/*.ts"],
14 | "migrations": ["src/migration/**/*.ts"],
15 | "subscribers": ["src/subscriber/**/*.ts"]
16 | }
17 |
--------------------------------------------------------------------------------
/backend/src/entity/Candidate.ts:
--------------------------------------------------------------------------------
1 | import {
2 | BaseEntity,
3 | Column,
4 | Entity,
5 | ManyToOne,
6 | PrimaryGeneratedColumn,
7 | } from "typeorm";
8 | import { Poll } from "./Poll";
9 |
10 | @Entity()
11 | export class Candidate extends BaseEntity {
12 | @PrimaryGeneratedColumn()
13 | id!: number;
14 |
15 | @Column()
16 | name!: string;
17 |
18 | @ManyToOne(() => Poll, (poll) => poll.candidates)
19 | poll!: Poll;
20 | }
21 |
--------------------------------------------------------------------------------
/backend/src/web3.ts:
--------------------------------------------------------------------------------
1 | import Web3 from "web3";
2 | import data from "../build/contracts/Election.json";
3 |
4 | export const web3 = new Web3("http://localhost:7545");
5 |
6 | const provider = new Web3.providers.HttpProvider("http://localhost:7545");
7 | const contract = require("@truffle/contract");
8 |
9 | const ElectionContract = contract(data);
10 |
11 | ElectionContract.setProvider(provider);
12 |
13 | export default ElectionContract;
14 |
--------------------------------------------------------------------------------
/backend/src/entity/Poll.ts:
--------------------------------------------------------------------------------
1 | import {
2 | BaseEntity,
3 | Column,
4 | Entity,
5 | OneToMany,
6 | PrimaryGeneratedColumn,
7 | } from "typeorm";
8 | import { Candidate } from "./Candidate";
9 |
10 | @Entity()
11 | export class Poll extends BaseEntity {
12 | @PrimaryGeneratedColumn()
13 | id!: number;
14 |
15 | @Column()
16 | name!: string;
17 |
18 | @OneToMany(() => Candidate, (candidate) => candidate.poll)
19 | candidates!: Candidate[];
20 | }
21 |
--------------------------------------------------------------------------------
/frontend/src/components/Back.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { IoIosArrowBack } from "react-icons/io";
3 |
4 | interface BackProps {
5 | call: (any: any) => any;
6 | }
7 |
8 | const Back = (props: BackProps) => {
9 | return (
10 |
11 |
12 |
13 |
14 | BACK
15 |
16 | );
17 | };
18 |
19 | export default Back;
20 |
--------------------------------------------------------------------------------
/backend/src/controllers/users/delete.ts:
--------------------------------------------------------------------------------
1 | import { Request, Response } from "express";
2 | import { User } from "../../entity/User";
3 |
4 | export default async (req: Request, res: Response) => {
5 | const { id } = req.params;
6 |
7 | if (!id) return res.status(400).send("no id found");
8 |
9 | try {
10 | await User.delete(id);
11 | } catch (error) {
12 | return res.status(400).send({ error });
13 | }
14 |
15 | return res.send({ userId: id });
16 | };
17 |
--------------------------------------------------------------------------------
/backend/src/routers/users.ts:
--------------------------------------------------------------------------------
1 | import { Router } from "express";
2 | import notVerifiedController from "../controllers/users/not-verified";
3 | import verifyController from "../controllers/users/verify";
4 | import deleteController from "../controllers/users/delete";
5 |
6 | const router = Router();
7 |
8 | router.get("/all", notVerifiedController);
9 | router.post("/verify", verifyController);
10 | router.delete("/delete/:id", deleteController);
11 |
12 | export default router;
13 |
--------------------------------------------------------------------------------
/backend/src/migration/1645187823173-users.ts:
--------------------------------------------------------------------------------
1 | import { MigrationInterface, QueryRunner } from "typeorm";
2 | import { User } from "../entity/User";
3 | import usersData from "../seed/users";
4 |
5 | export class users1645187823173 implements MigrationInterface {
6 | public async up(queryRunner: QueryRunner): Promise {
7 | const users = User.create(usersData);
8 | await User.save(users);
9 | }
10 |
11 | public async down(queryRunner: QueryRunner): Promise {}
12 | }
13 |
--------------------------------------------------------------------------------
/frontend/src/App.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { BrowserRouter } from "react-router-dom";
3 | import Footer from "./components/Footer";
4 | import AuthProvider from "./contexts/Auth";
5 | import CustomRoutes from "./components/CustomRoutes";
6 |
7 | const App = () => {
8 | return (
9 |
10 |
11 |
12 |
13 |
14 |
15 | );
16 | };
17 |
18 | export default App;
19 |
--------------------------------------------------------------------------------
/frontend/src/reportWebVitals.ts:
--------------------------------------------------------------------------------
1 | import { ReportHandler } from 'web-vitals';
2 |
3 | const reportWebVitals = (onPerfEntry?: ReportHandler) => {
4 | if (onPerfEntry && onPerfEntry instanceof Function) {
5 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
6 | getCLS(onPerfEntry);
7 | getFID(onPerfEntry);
8 | getFCP(onPerfEntry);
9 | getLCP(onPerfEntry);
10 | getTTFB(onPerfEntry);
11 | });
12 | }
13 | };
14 |
15 | export default reportWebVitals;
16 |
--------------------------------------------------------------------------------
/frontend/src/styles/components/Waiting.scss:
--------------------------------------------------------------------------------
1 | @keyframes rotate360 {
2 | 0% {
3 | transform: rotate(0deg);
4 | }
5 | 100% {
6 | transform: rotate(360deg);
7 | }
8 | }
9 |
10 | .waiting-wrapper {
11 | display: flex;
12 | justify-content: center;
13 | align-items: center;
14 |
15 | .cog {
16 | animation: rotate360 3s linear infinite;
17 |
18 | * {
19 | font-size: 20rem;
20 | color: $theme-primary;
21 | padding-top: 18px;
22 | margin: 0;
23 | }
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/backend/contracts/Migrations.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 | pragma solidity >=0.4.22 <0.9.0;
3 |
4 | contract Migrations {
5 | address public owner = msg.sender;
6 | uint public last_completed_migration;
7 |
8 | modifier restricted() {
9 | require(
10 | msg.sender == owner,
11 | "This function is restricted to the contract's owner"
12 | );
13 | _;
14 | }
15 |
16 | function setCompleted(uint completed) public restricted {
17 | last_completed_migration = completed;
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/backend/src/routers/auth.ts:
--------------------------------------------------------------------------------
1 | import { Router } from "express";
2 | import loginController from "../controllers/auth/login";
3 | import checkController from "../controllers/auth/check";
4 | import logoutController from "../controllers/auth/logout";
5 | import signupController from "../controllers/auth/signup";
6 |
7 | const router = Router();
8 |
9 | router.post("/login", loginController);
10 | router.post("/check", checkController);
11 | router.post("/logout", logoutController);
12 | router.post("/signup", signupController);
13 |
14 | export default router;
15 |
--------------------------------------------------------------------------------
/frontend/src/styles/layouts/Login.scss:
--------------------------------------------------------------------------------
1 | .login-layout-wrapper {
2 | display: grid;
3 | grid-template-columns: 1fr 1fr;
4 | padding-left: 100px;
5 | padding-right: 100px;
6 | height: 100vh;
7 |
8 | .left {
9 | position: relative;
10 | display: flex;
11 | flex-direction: column;
12 | justify-content: center;
13 |
14 | .title-green {
15 | color: $theme-primary;
16 | }
17 | }
18 |
19 | .right {
20 | display: flex;
21 | flex-direction: column;
22 | justify-content: center;
23 | align-items: center;
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/frontend/src/components/Polls/Panel.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import Chart from "./Chart";
3 |
4 | interface PanelProps {
5 | name: string;
6 | description: string;
7 | children: JSX.Element;
8 | }
9 |
10 | const Panel = (props: PanelProps) => {
11 | return (
12 |
13 |
{props.name}
14 |
{props.description}
15 |
16 |
{props.children}
17 |
18 | );
19 | };
20 |
21 | export default Panel;
22 |
--------------------------------------------------------------------------------
/backend/Readme.md:
--------------------------------------------------------------------------------
1 | # Running the project
2 |
3 | ## Required:
4 |
5 | - .env file (with ACCESS_TOKEN_SECRET and REFRESH_TOKEN_SECRET variables)
6 | - mysql database (edit the ormconfig.json)
7 |
8 | ## Run migration with:
9 |
10 | ```
11 | npm run typeorm migration:run
12 | ```
13 |
14 | ## API End-Points
15 |
16 | - /auth/login
17 | - /auth/check
18 | - /auth/logout
19 | - /auth/signup
20 | - /polls/
21 | - /polls/start
22 | - /polls/status
23 | - /polls/end
24 | - /polls/reset
25 | - /polls/voters
26 | - /polls/vote
27 | - /users/all
28 | - /users/verify
29 | - /users/delete/:id
30 |
--------------------------------------------------------------------------------
/backend/src/entity/User.ts:
--------------------------------------------------------------------------------
1 | import { BaseEntity, Column, Entity, PrimaryGeneratedColumn } from "typeorm";
2 |
3 | @Entity()
4 | export class User extends BaseEntity {
5 | @PrimaryGeneratedColumn()
6 | id!: number;
7 |
8 | @Column({ length: 100 })
9 | name!: string;
10 |
11 | @Column({ unique: true })
12 | citizenshipNumber!: string;
13 |
14 | @Column({ length: 180, unique: true })
15 | email!: string;
16 |
17 | @Column()
18 | password!: string;
19 |
20 | @Column()
21 | admin!: boolean;
22 |
23 | @Column({ default: false })
24 | verified!: boolean;
25 | }
26 |
--------------------------------------------------------------------------------
/backend/src/migration/1645175354681-user-update.ts:
--------------------------------------------------------------------------------
1 | import {MigrationInterface, QueryRunner} from "typeorm";
2 |
3 | export class userUpdate1645175354681 implements MigrationInterface {
4 | name = 'userUpdate1645175354681'
5 |
6 | public async up(queryRunner: QueryRunner): Promise {
7 | await queryRunner.query(`ALTER TABLE \`user\` ADD \`verified\` tinyint NOT NULL DEFAULT 0`);
8 | }
9 |
10 | public async down(queryRunner: QueryRunner): Promise {
11 | await queryRunner.query(`ALTER TABLE \`user\` DROP COLUMN \`verified\``);
12 | }
13 |
14 | }
15 |
--------------------------------------------------------------------------------
/frontend/src/index.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactDOM from "react-dom";
3 | import "./styles/index.scss";
4 | import App from "./App";
5 | import reportWebVitals from "./reportWebVitals";
6 |
7 | ReactDOM.render(
8 |
9 |
10 | ,
11 | document.getElementById("root")
12 | );
13 |
14 | // If you want to start measuring performance in your app, pass a function
15 | // to log results (for example: reportWebVitals(console.log))
16 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
17 | reportWebVitals();
18 |
--------------------------------------------------------------------------------
/backend/src/controllers/polls/reset.ts:
--------------------------------------------------------------------------------
1 | import { Request, Response } from "express";
2 | import ElectionContract, { web3 } from "../../web3";
3 |
4 | export default async (_: Request, res: Response) => {
5 | const accounts = await web3.eth.getAccounts();
6 | const instance = await ElectionContract.deployed();
7 |
8 | const status = await instance.getStatus();
9 |
10 | if (status !== "finished")
11 | return res.status(400).send("election not finished or already reset");
12 |
13 | await instance.resetElection({ from: accounts[0] });
14 |
15 | return res.send("successful");
16 | };
17 |
--------------------------------------------------------------------------------
/frontend/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/frontend/src/styles/vote-status.scss:
--------------------------------------------------------------------------------
1 | @keyframes blink {
2 | 0% {
3 | opacity: 1;
4 | }
5 |
6 | 50% {
7 | opacity: 0;
8 | }
9 |
10 | 100% {
11 | opacity: 1;
12 | }
13 | }
14 |
15 | .vote-status-wrapper {
16 | position: absolute;
17 | left: 20px;
18 | top: 20px;
19 |
20 | .running {
21 | width: 20px;
22 | height: 20px;
23 | background-color: $theme-primary;
24 | border-radius: 100px;
25 | animation: blink 1s linear infinite;
26 | }
27 |
28 | .finished {
29 | display: flex;
30 | justify-content: center;
31 | align-items: center;
32 | color: $theme-primary;
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/frontend/src/styles/layouts/Boxes.scss:
--------------------------------------------------------------------------------
1 | @mixin boxes-wrapper {
2 | padding-top: 100px;
3 | }
4 |
5 | @mixin box-wrapper {
6 | @include box-shadow;
7 | min-width: 400px;
8 | padding: 20px;
9 | margin: 20px;
10 | border-radius: 10px;
11 | cursor: pointer;
12 | transition: 0.3s;
13 |
14 | &:hover {
15 | transform: scale(1.1);
16 | }
17 | }
18 |
19 | @mixin inner-box-wrapper {
20 | display: flex;
21 | align-items: flex-start;
22 | padding-top: 20px;
23 | }
24 |
25 | @mixin inner-box-name {
26 | padding: 10px 20px 10px 20px;
27 | margin-right: 10px;
28 | background-color: $theme-gray;
29 | border-radius: 100px;
30 | }
31 |
--------------------------------------------------------------------------------
/frontend/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": [
5 | "dom",
6 | "dom.iterable",
7 | "esnext"
8 | ],
9 | "allowJs": true,
10 | "skipLibCheck": true,
11 | "esModuleInterop": true,
12 | "allowSyntheticDefaultImports": true,
13 | "strict": true,
14 | "forceConsistentCasingInFileNames": true,
15 | "noFallthroughCasesInSwitch": true,
16 | "module": "esnext",
17 | "moduleResolution": "node",
18 | "resolveJsonModule": true,
19 | "isolatedModules": true,
20 | "noEmit": true,
21 | "jsx": "react-jsx"
22 | },
23 | "include": [
24 | "src"
25 | ]
26 | }
27 |
--------------------------------------------------------------------------------
/backend/src/controllers/polls/end.ts:
--------------------------------------------------------------------------------
1 | import { Request, Response } from "express";
2 | import ElectionContract, { web3 } from "../../web3";
3 | import memoryCache from "memory-cache";
4 |
5 | export default async (_: Request, res: Response) => {
6 | const accounts = await web3.eth.getAccounts();
7 | const instance = await ElectionContract.deployed();
8 |
9 | const status = await instance.getStatus();
10 |
11 | if (status !== "running") return res.status(400).send("election not started");
12 |
13 | await instance.endElection({ from: accounts[0] });
14 |
15 | const votes = await instance.getVotes();
16 |
17 | memoryCache.clear();
18 |
19 | return res.send({ votes });
20 | };
21 |
--------------------------------------------------------------------------------
/frontend/src/styles/pages/Features.scss:
--------------------------------------------------------------------------------
1 | .features-wrapper {
2 | display: flex;
3 | flex-direction: column;
4 | justify-content: center;
5 | align-items: center;
6 | padding-top: 100px;
7 | padding-left: 50px;
8 | padding-right: 50px;
9 |
10 | .mobile-wrapper {
11 | width: 100%;
12 | display: grid;
13 | grid-template-columns: repeat(3, 1fr);
14 | padding-top: 50px;
15 |
16 | & > div {
17 | display: flex;
18 | align-items: center;
19 | }
20 |
21 | .mobile-container {
22 | grid-row: 1/3;
23 | grid-column: 2/3;
24 | display: flex;
25 | justify-content: center;
26 | align-items: center;
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/backend/src/middlewares/cache.ts:
--------------------------------------------------------------------------------
1 | import { NextFunction, Request, Response, Send } from "express";
2 | import memoryCache from "memory-cache";
3 |
4 | export default (duration: number) => {
5 | return (req: Request, res: Response, next: NextFunction) => {
6 | const key = "__route__" + req.originalUrl || req.url;
7 |
8 | const cache = memoryCache.get(key);
9 | if (cache) return res.send(cache);
10 |
11 | // if no cache-hit then
12 |
13 | const temp = res.send;
14 |
15 | const customSendFunction: Send = (body) => {
16 | memoryCache.put(key, body, duration * 1000);
17 | return temp(body);
18 | };
19 |
20 | res.send = customSendFunction;
21 |
22 | next();
23 | };
24 | };
25 |
--------------------------------------------------------------------------------
/frontend/src/styles/pages/User/Profile.scss:
--------------------------------------------------------------------------------
1 | .profile-wrapper {
2 | @include box-shadow;
3 | padding: 50px;
4 | display: grid;
5 | border-radius: 10px;
6 | box-sizing: border-box;
7 | grid-template-columns: 1fr 2fr;
8 | column-gap: 100px;
9 |
10 | .left-panel {
11 | text-align: center;
12 |
13 | .person-icon {
14 | * {
15 | font-size: 4rem;
16 | color: $theme-primary;
17 | }
18 | }
19 |
20 | .username {
21 | padding: 20px 0 20px 0;
22 | }
23 | }
24 |
25 | .right-panel {
26 | .skeleton {
27 | height: 30px;
28 | width: 200px;
29 | background: $theme-gray;
30 | margin-top: 30px;
31 | border-radius: 10px;
32 | }
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/backend/src/server.ts:
--------------------------------------------------------------------------------
1 | import express, { Request, Response } from "express";
2 | import cookieParser from "cookie-parser";
3 | import cors from "cors";
4 |
5 | import authRouter from "./routers/auth";
6 | import pollsRouter from "./routers/polls";
7 | import usersRouter from "./routers/users";
8 |
9 | const app = express();
10 |
11 | app.use(cors({ origin: "http://localhost:3000", credentials: true }));
12 | app.use(express.json());
13 | app.use(cookieParser());
14 | app.use("/auth", authRouter);
15 | app.use("/polls", pollsRouter);
16 | app.use("/users", usersRouter);
17 |
18 | app.get("/", (req: Request, res: Response) => {
19 | console.log(req.cookies);
20 | res.status(404).send("no link matched!");
21 | });
22 |
23 | export default app;
24 |
--------------------------------------------------------------------------------
/frontend/src/styles/pages/Landing.scss:
--------------------------------------------------------------------------------
1 | .landing {
2 | display: grid;
3 | grid-template-columns: 1fr 1fr;
4 |
5 | .left {
6 | padding: 100px;
7 | padding-top: 20px;
8 | background: $theme-primary;
9 | color: white;
10 | display: flex;
11 | flex-direction: column;
12 | justify-content: center;
13 |
14 | .logo {
15 | padding-bottom: 50px;
16 | display: flex;
17 | align-items: center;
18 | font-size: 30px;
19 | transform: translateX(-14px);
20 |
21 | img {
22 | width: 150px;
23 | }
24 | }
25 |
26 | .button-wrapper {
27 | padding-top: 50px;
28 | }
29 | }
30 |
31 | .right {
32 | display: flex;
33 | justify-content: right;
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/backend/src/controllers/polls/votes.ts:
--------------------------------------------------------------------------------
1 | import { Request, Response } from "express";
2 | import ElectionContract from "../../web3";
3 |
4 | export default async (_: Request, res: Response) => {
5 | const instance = await ElectionContract.deployed();
6 |
7 | const candidates = await instance.getCandidates();
8 | const votes = await instance.getVotes();
9 |
10 | const response: any = {};
11 |
12 | for (let i = 0; i < candidates.length; i++) {
13 | response[candidates[i]] = 0;
14 | }
15 |
16 | for (let i = 0; i < votes.length; i++) {
17 | const vote = votes[i];
18 |
19 | if (typeof response[vote[3]] != "undefined")
20 | response[vote[3]] = response[vote[3]] + 1;
21 | }
22 |
23 | return res.send({ votes: response });
24 | };
25 |
--------------------------------------------------------------------------------
/frontend/src/styles/pages/Admin/Create.scss:
--------------------------------------------------------------------------------
1 | .add-candidate-wrapper {
2 | display: flex;
3 | justify-content: right;
4 | padding: 0;
5 | margin: 0;
6 |
7 | input {
8 | flex: 3;
9 | margin: 0;
10 | }
11 |
12 | button {
13 | flex: 1;
14 | margin: 0;
15 | margin-left: 10px;
16 | }
17 | }
18 |
19 | .candidates-container {
20 | padding-bottom: 20px;
21 | display: flex;
22 |
23 | .candidate-wrapper {
24 | padding: 10px 20px 10px 20px;
25 | margin-right: 10px;
26 | background-color: $theme-gray;
27 | border-radius: 100px;
28 | display: flex;
29 | align-items: center;
30 |
31 | .remove {
32 | font-size: 0.8rem;
33 | padding-left: 10px;
34 | cursor: pointer;
35 | }
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/frontend/src/pages/Result.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from "react";
2 | import axios from "../axios";
3 | import Chart from "../components/Polls/Chart";
4 | import Panel from "../components/Polls/Panel";
5 |
6 | const Result = () => {
7 | const [loading, setLoading] = useState(true);
8 | const [data, setData] = useState({ name: "", description: "", votes: {} });
9 |
10 | useEffect(() => {
11 | axios.get("/polls/").then((res) => {
12 | setData(res.data);
13 | setLoading(false);
14 | });
15 | }, []);
16 |
17 | if (loading) return
;
18 |
19 | return (
20 |
21 |
22 |
23 | );
24 | };
25 |
26 | export default Result;
27 |
--------------------------------------------------------------------------------
/backend/src/controllers/users/verify.ts:
--------------------------------------------------------------------------------
1 | import { Request, Response } from "express";
2 | import { User } from "../../entity/User";
3 | import * as yup from "yup";
4 |
5 | const schema = yup.object({
6 | body: yup.object({
7 | userId: yup.number().integer().required(),
8 | }),
9 | });
10 |
11 | export default async (req: Request, res: Response) => {
12 | try {
13 | await schema.validate(req);
14 | } catch (error: any) {
15 | return res.status(400).send(error.errors);
16 | }
17 |
18 | let user;
19 |
20 | try {
21 | user = await User.findOneOrFail({ where: { id: req.body.userId } });
22 | } catch (error) {
23 | return res.status(400).send({ error });
24 | }
25 |
26 | user.verified = true;
27 |
28 | await User.save(user);
29 |
30 | return res.send({ user });
31 | };
32 |
--------------------------------------------------------------------------------
/frontend/src/pages/Polls.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from "react";
2 | import Panel from "../components/Polls/Panel";
3 | import Chart from "../components/Polls/Chart";
4 | import axios from "../axios";
5 |
6 | const Polls = () => {
7 | const [loading, setLoading] = useState(true);
8 | const [data, setData] = useState({ name: "", description: "", votes: {} });
9 |
10 | useEffect(() => {
11 | axios
12 | .get("/polls/")
13 | .then((res) => {
14 | setData(res.data);
15 | setLoading(false);
16 | })
17 | .catch((err) => console.log({ err }));
18 | }, []);
19 |
20 | if (loading) return
;
21 |
22 | return (
23 |
24 |
25 |
26 | );
27 | };
28 |
29 | export default Polls;
30 |
--------------------------------------------------------------------------------
/backend/src/controllers/polls/fetch.ts:
--------------------------------------------------------------------------------
1 | import { Request, Response } from "express";
2 | import ElectionContract from "../../web3";
3 |
4 | export default async (req: Request, res: Response) => {
5 | const instance = await ElectionContract.deployed();
6 | const name = await instance.getElectionName();
7 | const description = await instance.getElectionDescription();
8 |
9 | const candidates = await instance.getCandidates();
10 | const votes = await instance.getVotes();
11 |
12 | const response: any = {};
13 |
14 | for (let i = 0; i < candidates.length; i++) {
15 | response[candidates[i]] = 0;
16 | }
17 |
18 | for (let i = 0; i < votes.length; i++) {
19 | const vote = votes[i];
20 |
21 | if (typeof response[vote[3]] != "undefined")
22 | response[vote[3]] = response[vote[3]] + 1;
23 | }
24 |
25 | return res.send({ name, description, votes: response });
26 | };
27 |
--------------------------------------------------------------------------------
/backend/src/routers/polls.ts:
--------------------------------------------------------------------------------
1 | import express from "express";
2 |
3 | import startController from "../controllers/polls/start";
4 | import fetchController from "../controllers/polls/fetch";
5 | import statusController from "../controllers/polls/status";
6 | import endController from "../controllers/polls/end";
7 | import resetController from "../controllers/polls/reset";
8 | import votesController from "../controllers/polls/votes";
9 | import voteController, { checkVoteability } from "../controllers/polls/vote";
10 |
11 | const router = express.Router();
12 |
13 | router.get("/", fetchController);
14 | router.get("/status", statusController);
15 | router.get("/votes", votesController);
16 |
17 | router.post("/start", startController);
18 | router.post("/end", endController);
19 | router.post("/reset", resetController);
20 | router.post("/check-voteability", checkVoteability);
21 | router.post("/vote", voteController);
22 |
23 | export default router;
24 |
--------------------------------------------------------------------------------
/frontend/src/pages/Admin/Verify.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { useParams } from "react-router";
3 | import axios from "../../axios";
4 |
5 | const Verify = () => {
6 | const { id, name } = useParams();
7 |
8 | const verifyUser = () => {
9 | axios
10 | .post("/users/verify", { userId: id })
11 | .then((res) => console.log({ res }))
12 | .catch((error) => console.log({ error }));
13 | };
14 |
15 | const deleteUser = () => {
16 | axios
17 | .delete(`/users/delete/${id}`)
18 | .then((res) => console.log({ res }))
19 | .catch((error) => console.log({ error }));
20 | };
21 |
22 | return (
23 |
24 |
25 | verify {name}
26 |
27 |
28 |
29 | delete {name}
30 |
31 |
32 | );
33 | };
34 |
35 | export default Verify;
36 |
--------------------------------------------------------------------------------
/frontend/src/styles/components/Polls/Chart.scss:
--------------------------------------------------------------------------------
1 | .bars-container {
2 | min-width: 800px;
3 | display: flex;
4 | justify-content: center;
5 |
6 | .bar-wrapper {
7 | width: 50px;
8 | margin-left: 50px;
9 | margin-right: 50px;
10 | background-color: $theme-gray;
11 | height: 300px;
12 | display: flex;
13 | flex-direction: column;
14 | justify-content: flex-end;
15 | border-radius: 10px;
16 | overflow: hidden;
17 | }
18 | }
19 |
20 | .buttons-wrapper {
21 | min-width: 800px;
22 | display: flex;
23 | justify-content: center;
24 |
25 | .button-wrapper {
26 | margin-top: 20px;
27 | // width: 50px;
28 | margin-left: 50px;
29 | margin-right: 50px;
30 | }
31 | }
32 |
33 | .names-wrapper {
34 | min-width: 800px;
35 | display: flex;
36 | justify-content: center;
37 |
38 | .name-wrapper {
39 | padding-top: 20px;
40 | width: 50px;
41 | margin-left: 50px;
42 | margin-right: 50px;
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/frontend/src/components/Home/Landing.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Link } from "react-router-dom";
3 |
4 | const Landing = () => {
5 | return (
6 |
7 |
8 |
9 |
10 |
11 |
12 |
Blockchain Based
13 |
Voting System
14 |
the future of voting
15 |
16 |
17 |
18 | Login
19 |
20 |
21 |
22 | View Votes
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 | );
32 | };
33 |
34 | export default Landing;
35 |
--------------------------------------------------------------------------------
/backend/src/seed/users.ts:
--------------------------------------------------------------------------------
1 | type UserType = {
2 | name: string;
3 | email: string;
4 | password: string;
5 | citizenshipNumber: string;
6 | admin: boolean;
7 | verified: boolean;
8 | };
9 |
10 | const users: UserType[] = [
11 | {
12 | name: "John",
13 | citizenshipNumber: "9860777906",
14 | email: "john@gmail.com",
15 | password: "$2b$10$6sdkothEwAguhA0FytsGF.gcWPmTDB5hosif6rGX5FFJK8PdBgRHu",
16 | admin: true,
17 | verified: true,
18 | },
19 | {
20 | name: "Liza",
21 | citizenshipNumber: "9860777907",
22 | email: "liza@gmail.com",
23 | password: "$2b$10$70yLw0dPhAD0py/iiGUInO7kklGUmbMfa5BmXKGCXEID1ufTsqSQ6",
24 | admin: false,
25 | verified: true,
26 | },
27 | {
28 | name: "Ben",
29 | citizenshipNumber: "9860777908",
30 | email: "ben@gmail.com",
31 | password: "$2b$10$1DsQFSqUs3ufyDDRBd9wYuU5i9ihbnYR4GCYJsI3IzGXamwFWnr4S",
32 | admin: false,
33 | verified: true,
34 | },
35 | ];
36 |
37 | export default users;
38 |
--------------------------------------------------------------------------------
/frontend/src/components/Features/Feature.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | type FeatureProps = {
4 | icon: JSX.Element;
5 | title: string;
6 | align: "left" | "right";
7 | children: JSX.Element;
8 | };
9 |
10 | const Feature = (props: FeatureProps) => {
11 | const iconContainer = {props.icon}
;
12 |
13 | const featureInfo = (
14 |
15 |
{props.title}
16 |
{props.children}
17 |
18 | );
19 | return (
20 |
21 | {props.align === "left" ? (
22 | <>
23 | {iconContainer}
24 |
{featureInfo}
25 | >
26 | ) : (
27 | <>
28 |
{featureInfo}
29 | {iconContainer}
30 | >
31 | )}
32 |
33 | );
34 | };
35 |
36 | export default Feature;
37 |
--------------------------------------------------------------------------------
/frontend/src/pages/Admin/Home.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from "react";
2 | import { RouteProps } from "react-router";
3 | import axios from "../../axios";
4 | import StartPage from "./Start";
5 | import PollsPage from "./Polls";
6 | import ResultPage from "./Result";
7 |
8 | const Home = (props: RouteProps): JSX.Element => {
9 | const [loading, setLoading] = useState(true);
10 | const [status, setStatus] = useState<"not-started" | "running" | "finished">(
11 | "not-started"
12 | );
13 |
14 | useEffect(() => {
15 | setLoading(true);
16 | axios
17 | .get("/polls/status")
18 | .then((res) => {
19 | setStatus(res.data.status);
20 | setLoading(false);
21 | })
22 | .catch((error) => console.log({ error }));
23 | }, []);
24 |
25 | if (loading) return
;
26 |
27 | if (status === "finished") return ;
28 | if (status === "running") return ;
29 |
30 | return ;
31 | };
32 |
33 | export default Home;
34 |
--------------------------------------------------------------------------------
/frontend/src/styles/form.scss:
--------------------------------------------------------------------------------
1 | .form-container {
2 | @include box-shadow;
3 | min-width: 400px;
4 | box-sizing: border-box;
5 | padding: 20px;
6 | border-radius: 20px;
7 |
8 | .input-container {
9 | padding-bottom: 20px;
10 |
11 | input {
12 | border: none;
13 | outline: none;
14 | background: $theme-gray;
15 | padding: 10px;
16 | border-radius: 5px;
17 | width: 96%;
18 | font-family: "Josefin Sans", sans-serif;
19 |
20 | &::placeholder {
21 | opacity: 0.5;
22 | }
23 | }
24 | }
25 |
26 | button {
27 | width: 100%;
28 | }
29 |
30 | hr {
31 | margin: 30px;
32 | border: none;
33 | outline: none;
34 | border-top: 2px solid $theme-gray;
35 | }
36 |
37 | .form-info-text {
38 | text-align: center;
39 | color: $theme-primary;
40 | margin: 30px;
41 | }
42 | }
43 |
44 | .form-error-text {
45 | color: $theme-secondary;
46 | text-transform: lowercase;
47 | font-weight: 400;
48 | font-size: 0.9rem;
49 | }
50 |
--------------------------------------------------------------------------------
/backend/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "scripts": {
3 | "start": "nodemon src/index.ts",
4 | "typeorm": "node --require ts-node/register ./node_modules/typeorm/cli.js"
5 | },
6 | "dependencies": {
7 | "@truffle/contract": "^4.4.9",
8 | "bcrypt": "^5.0.1",
9 | "cookie-parser": "^1.4.6",
10 | "cors": "^2.8.5",
11 | "dayjs": "^1.10.7",
12 | "dotenv": "^16.0.0",
13 | "express": "^4.17.2",
14 | "jsonwebtoken": "^8.5.1",
15 | "memory-cache": "^0.2.0",
16 | "mysql": "^2.18.1",
17 | "reflect-metadata": "^0.1.13",
18 | "solc": "^0.8.12",
19 | "typeorm": "^0.2.41",
20 | "web3": "^1.7.1",
21 | "yup": "^0.32.11"
22 | },
23 | "devDependencies": {
24 | "@types/bcrypt": "^5.0.0",
25 | "@types/cookie-parser": "^1.4.2",
26 | "@types/cors": "^2.8.12",
27 | "@types/express": "^4.17.13",
28 | "@types/jsonwebtoken": "^8.5.8",
29 | "@types/memory-cache": "^0.2.2",
30 | "@types/node": "^17.0.16",
31 | "@types/web3": "^1.2.2",
32 | "nodemon": "^2.0.15",
33 | "ts-node": "^10.4.0",
34 | "typescript": "^4.5.5"
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/frontend/src/pages/User/Profile.tsx:
--------------------------------------------------------------------------------
1 | import React, { useContext } from "react";
2 | import { RouteProps } from "react-router";
3 | import { AuthContext } from "../../contexts/Auth";
4 |
5 | const Profile = (props: RouteProps) => {
6 | const authContext = useContext(AuthContext);
7 |
8 | console.log({ authContext });
9 |
10 | return (
11 |
12 |
13 |
14 |
15 |
16 |
{authContext.name}
17 |
18 | Logout
19 |
20 |
21 |
22 |
23 |
Profile
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 | );
32 | };
33 |
34 | export default Profile;
35 |
--------------------------------------------------------------------------------
/frontend/src/components/Navbar.tsx:
--------------------------------------------------------------------------------
1 | import React, { useContext } from "react";
2 | import { useNavigate } from "react-router";
3 | import { AuthContext } from "../contexts/Auth";
4 |
5 | type NavbarContainerProps = {
6 | children: JSX.Element;
7 | };
8 |
9 | const NavbarContainer = (props: NavbarContainerProps) => {
10 | const navigate = useNavigate();
11 |
12 | return (
13 |
14 | LOGO
15 | {props.children}
16 | navigate("/profile")}>profile
17 |
18 | );
19 | };
20 |
21 | const Navbar = () => {
22 | const authContext = useContext(AuthContext);
23 |
24 | const getNavbar = (): JSX.Element => {
25 | if (!authContext.authenticated) {
26 | return
;
27 | }
28 |
29 | if (authContext.isAdmin) {
30 | return (
31 |
32 | admin
33 |
34 | );
35 | }
36 |
37 | return (
38 |
39 | user
40 |
41 | );
42 | };
43 |
44 | return getNavbar();
45 | };
46 |
47 | export default Navbar;
48 |
--------------------------------------------------------------------------------
/frontend/src/pages/Admin/Polls.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from "react";
2 | import axios from "../../axios";
3 | import Chart from "../../components/Polls/Chart";
4 | import Panel from "../../components/Polls/Panel";
5 |
6 | const Polls = () => {
7 | const [loading, setLoading] = useState(true);
8 | const [data, setData] = useState({ name: "", description: "", votes: {} });
9 |
10 | useEffect(() => {
11 | axios.get("/polls/").then((res) => {
12 | setData(res.data);
13 | setLoading(false);
14 | });
15 | }, []);
16 |
17 | const endElection = () => {
18 | axios
19 | .post("/polls/end")
20 | .then((_) => window.location.reload())
21 | .catch((err) => console.log({ err }));
22 | };
23 |
24 | if (loading) return
;
25 |
26 | return (
27 |
28 | <>
29 |
30 |
31 |
35 | End Election
36 |
37 | >
38 |
39 | );
40 | };
41 |
42 | export default Polls;
43 |
--------------------------------------------------------------------------------
/frontend/src/pages/Admin/Result.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from "react";
2 | import axios from "../../axios";
3 | import Chart from "../../components/Polls/Chart";
4 | import Panel from "../../components/Polls/Panel";
5 |
6 | const Result = () => {
7 | const [loading, setLoading] = useState(true);
8 | const [data, setData] = useState({ name: "", description: "", votes: {} });
9 |
10 | useEffect(() => {
11 | axios.get("/polls/").then((res) => {
12 | setData(res.data);
13 | setLoading(false);
14 | });
15 | }, []);
16 |
17 | const resetElection = () => {
18 | axios
19 | .post("/polls/reset")
20 | .then((_) => window.location.reload())
21 | .catch((err) => console.log({ err }));
22 | };
23 |
24 | if (loading) return
;
25 |
26 | return (
27 |
28 | <>
29 |
30 |
31 |
35 | Reset Election
36 |
37 | >
38 |
39 | );
40 | };
41 |
42 | export default Result;
43 |
--------------------------------------------------------------------------------
/backend/src/controllers/auth/signup.ts:
--------------------------------------------------------------------------------
1 | import { Request, Response } from "express";
2 | import * as yup from "yup";
3 | import { User } from "../../entity/User";
4 | import bcrypt from "bcrypt";
5 |
6 | const schema = yup.object({
7 | body: yup.object({
8 | name: yup.string().min(3).required(),
9 | email: yup.string().email().required(),
10 | password: yup.string().min(3).required(),
11 | citizenshipNumber: yup.string().min(4),
12 | }),
13 | });
14 |
15 | export default async (req: Request, res: Response) => {
16 | try {
17 | await schema.validate(req);
18 | } catch (error: any) {
19 | return res.status(400).send(error.errors);
20 | }
21 |
22 | let hashedPassword = undefined;
23 |
24 | try {
25 | hashedPassword = await bcrypt.hash(req.body.password, 10);
26 | } catch (error) {
27 | return res.status(500).send({ error });
28 | }
29 |
30 | const newUser = new User();
31 |
32 | newUser.admin = false;
33 | newUser.name = req.body.name;
34 | newUser.email = req.body.email;
35 | newUser.password = hashedPassword;
36 | newUser.citizenshipNumber = req.body.citizenshipNumber;
37 |
38 | try {
39 | await User.save(newUser);
40 | } catch (error) {
41 | return res.status(400).send(error);
42 | }
43 |
44 | return res.send(newUser);
45 | };
46 |
--------------------------------------------------------------------------------
/frontend/src/pages/View.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from "react";
2 | import { RouteProps, useNavigate } from "react-router";
3 | import axios from "../axios";
4 | import Polls from "./Polls";
5 | import Result from "./Result";
6 | import Start from "./Start";
7 | import Back from "../components/Back";
8 |
9 | const View = (props: RouteProps): JSX.Element => {
10 | const [loading, setLoading] = useState(true);
11 | const [status, setStatus] = useState<"not-started" | "running" | "finished">(
12 | "not-started"
13 | );
14 |
15 | const navigate = useNavigate();
16 |
17 | useEffect(() => {
18 | setLoading(true);
19 | axios
20 | .get("/polls/status")
21 | .then((res) => {
22 | setStatus(res.data.status);
23 | setLoading(false);
24 | })
25 | .catch((error) => console.log({ error }));
26 | }, []);
27 |
28 | let comp =
;
29 |
30 | if (loading) return comp;
31 |
32 | if (status === "finished") comp = ;
33 | if (status === "running") comp = ;
34 | if (status === "not-started") comp = ;
35 |
36 | return (
37 |
38 |
navigate("/")} />
39 | {comp}
40 |
41 | );
42 | };
43 |
44 | export default View;
45 |
--------------------------------------------------------------------------------
/frontend/src/styles/layouts/Default.scss:
--------------------------------------------------------------------------------
1 | .default-container {
2 | min-height: 80vh;
3 | display: flex;
4 |
5 | .default-sidebar-container {
6 | width: 100px;
7 |
8 | .hamburger {
9 | font-size: 2rem;
10 | color: $theme-black;
11 | padding: 20px;
12 | cursor: pointer;
13 | }
14 |
15 | .default-sidebar {
16 | position: fixed;
17 | z-index: 1000;
18 | left: 0;
19 | top: 0;
20 | height: 100vh;
21 | width: 30vw;
22 | transition: 0.3s;
23 | background-color: $theme-primary;
24 | color: white;
25 | box-shadow: 0 0 10px rgba(50, 50, 50, 0.4);
26 |
27 | .hamburger {
28 | font-size: 2rem;
29 | color: white;
30 | }
31 |
32 | .default-sidebar-link {
33 | padding: 20px;
34 | text-transform: uppercase;
35 | cursor: pointer;
36 | }
37 | }
38 | }
39 |
40 | .default-content {
41 | flex: 1;
42 | padding-right: 100px;
43 | display: flex;
44 | justify-content: center;
45 | align-items: center;
46 | }
47 | }
48 |
49 | .display {
50 | // visibility: visible;
51 | // opacity: 1;
52 | transform: translateX(0);
53 | }
54 |
55 | .hide {
56 | // visibility: hidden;
57 | // opacity: 0;
58 | transform: translateX(-800px);
59 | }
60 |
61 | .active {
62 | background-color: $theme-secondary;
63 | }
64 |
--------------------------------------------------------------------------------
/backend/src/controllers/polls/start.ts:
--------------------------------------------------------------------------------
1 | import { Request, Response } from "express";
2 | import * as yup from "yup";
3 | import ElectionContract, { web3 } from "../../web3";
4 |
5 | const schema = yup.object({
6 | body: yup.object({
7 | name: yup.string().min(3).required(),
8 | description: yup.string().min(10).required(),
9 | candidates: yup.array(
10 | yup.object({
11 | name: yup.string().min(3),
12 | info: yup.string().min(10),
13 | })
14 | ),
15 | }),
16 | });
17 |
18 | export default async (req: Request, res: Response) => {
19 | try {
20 | await schema.validate(req);
21 | } catch (error: any) {
22 | return res.status(400).send(error.errors);
23 | }
24 |
25 | const instance = await ElectionContract.deployed();
26 |
27 | const status = await instance.getStatus();
28 | if (status !== "not-started")
29 | return res.status(400).send("election already started or not reset");
30 |
31 | const accounts = await web3.eth.getAccounts();
32 |
33 | await instance.setElectionDetails(req.body.name, req.body.description, {
34 | from: accounts[0],
35 | });
36 |
37 | for (let i = 0; i < req.body.candidates.length; i++) {
38 | const candidate = req.body.candidates[i];
39 | await instance.addCandidate(candidate.name, candidate.info, {
40 | from: accounts[0],
41 | });
42 | }
43 |
44 | return res.send(req.body);
45 | };
46 |
--------------------------------------------------------------------------------
/frontend/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "frontend",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@testing-library/jest-dom": "^5.16.1",
7 | "@testing-library/react": "^12.1.2",
8 | "@testing-library/user-event": "^13.5.0",
9 | "@types/jest": "^27.4.0",
10 | "@types/node": "^16.11.18",
11 | "@types/react": "^17.0.38",
12 | "@types/react-dom": "^17.0.11",
13 | "@types/react-router": "^5.1.18",
14 | "@types/react-router-dom": "^5.3.3",
15 | "axios": "^0.25.0",
16 | "bootstrap-icons": "^1.8.1",
17 | "formik": "^2.2.9",
18 | "node-sass": "^7.0.1",
19 | "react": "^17.0.2",
20 | "react-dom": "^17.0.2",
21 | "react-icons": "^4.3.1",
22 | "react-router-dom": "^6.2.1",
23 | "react-scripts": "5.0.0",
24 | "typescript": "^4.5.4",
25 | "web-vitals": "^2.1.2",
26 | "yup": "^0.32.11"
27 | },
28 | "scripts": {
29 | "start": "react-scripts start",
30 | "build": "react-scripts build",
31 | "test": "react-scripts test",
32 | "eject": "react-scripts eject"
33 | },
34 | "eslintConfig": {
35 | "extends": [
36 | "react-app",
37 | "react-app/jest"
38 | ]
39 | },
40 | "browserslist": {
41 | "production": [
42 | ">0.2%",
43 | "not dead",
44 | "not op_mini all"
45 | ],
46 | "development": [
47 | "last 1 chrome version",
48 | "last 1 firefox version",
49 | "last 1 safari version"
50 | ]
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/frontend/src/layouts/Login.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { useNavigate } from "react-router";
3 | import BackButton from "../components/Back";
4 |
5 | interface LayoutProps {
6 | error: string;
7 | success?: string;
8 | children: JSX.Element;
9 | }
10 |
11 | const Login = (props: LayoutProps) => {
12 | const navigate = useNavigate();
13 |
14 | return (
15 |
16 |
17 |
navigate("/")} />
18 |
19 | Blockchain Based
20 | Voting System
21 | the future of voting
22 |
23 |
24 |
25 | {props.error !== "" ? (
26 |
27 |
28 |
29 |
30 | {props.error} ...
31 |
32 | ) : null}
33 |
34 | {props.success && props.success !== "" ? (
35 |
36 |
37 |
38 |
39 | {props.success} ...
40 |
41 | ) : null}
42 |
43 |
{props.children}
44 |
45 |
46 | );
47 | };
48 |
49 | export default Login;
50 |
--------------------------------------------------------------------------------
/backend/src/migration/1645167790939-initial.ts:
--------------------------------------------------------------------------------
1 | import {MigrationInterface, QueryRunner} from "typeorm";
2 |
3 | export class initial1645167790939 implements MigrationInterface {
4 | name = 'initial1645167790939'
5 |
6 | public async up(queryRunner: QueryRunner): Promise {
7 | await queryRunner.query(`CREATE TABLE \`candidate\` (\`id\` int NOT NULL AUTO_INCREMENT, \`name\` varchar(255) NOT NULL, \`pollId\` int NULL, PRIMARY KEY (\`id\`)) ENGINE=InnoDB`);
8 | await queryRunner.query(`CREATE TABLE \`poll\` (\`id\` int NOT NULL AUTO_INCREMENT, \`name\` varchar(255) NOT NULL, PRIMARY KEY (\`id\`)) ENGINE=InnoDB`);
9 | await queryRunner.query(`CREATE TABLE \`user\` (\`id\` int NOT NULL AUTO_INCREMENT, \`name\` varchar(100) NOT NULL, \`citizenshipNumber\` varchar(255) NOT NULL, \`email\` varchar(180) NOT NULL, \`password\` varchar(255) NOT NULL, \`admin\` tinyint NOT NULL, UNIQUE INDEX \`IDX_5cccbf6f2ad61b287544ddf45d\` (\`citizenshipNumber\`), UNIQUE INDEX \`IDX_e12875dfb3b1d92d7d7c5377e2\` (\`email\`), PRIMARY KEY (\`id\`)) ENGINE=InnoDB`);
10 | await queryRunner.query(`ALTER TABLE \`candidate\` ADD CONSTRAINT \`FK_01e0d963db31f953b94d0446e17\` FOREIGN KEY (\`pollId\`) REFERENCES \`poll\`(\`id\`) ON DELETE NO ACTION ON UPDATE NO ACTION`);
11 | }
12 |
13 | public async down(queryRunner: QueryRunner): Promise {
14 | await queryRunner.query(`ALTER TABLE \`candidate\` DROP FOREIGN KEY \`FK_01e0d963db31f953b94d0446e17\``);
15 | await queryRunner.query(`DROP INDEX \`IDX_e12875dfb3b1d92d7d7c5377e2\` ON \`user\``);
16 | await queryRunner.query(`DROP INDEX \`IDX_5cccbf6f2ad61b287544ddf45d\` ON \`user\``);
17 | await queryRunner.query(`DROP TABLE \`user\``);
18 | await queryRunner.query(`DROP TABLE \`poll\``);
19 | await queryRunner.query(`DROP TABLE \`candidate\``);
20 | }
21 |
22 | }
23 |
--------------------------------------------------------------------------------
/frontend/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
17 |
18 |
27 | React App
28 |
29 |
30 | You need to enable JavaScript to run this app.
31 |
32 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/backend/src/controllers/auth/check.ts:
--------------------------------------------------------------------------------
1 | import { Request, Response } from "express";
2 | import jwt from "jsonwebtoken";
3 | import dayjs from "dayjs";
4 |
5 | export default async (req: Request, res: Response) => {
6 | const refreshToken = req.cookies.refreshToken;
7 |
8 | if (!refreshToken) return res.status(400).send("not authenticated");
9 |
10 | try {
11 | const accessTokenSecret = process.env.ACCESS_TOKEN_SECRET;
12 | const refreshTokenSecret = process.env.REFRESH_TOKEN_SECRET;
13 |
14 | if (!accessTokenSecret || !refreshTokenSecret) {
15 | console.log("did you forget to add .env file to the project?");
16 | console.log(`
17 | add the following:
18 |
19 | ACCESS_TOKEN_SECRET=976a66a5bd23b2050019f380c4decbbefdf8ff91cf502c68a3fe1ced91d7448cc54ce6c847657d53294e40889cef5bd996ec5b0fefc1f56270e06990657eeb6e
20 |
21 | REFRESH_TOKEN_SECRET=5f567afa6406225c4a759daae77e07146eca5df8149353a844fa9ab67fba22780cb4baa5ea508214934531a6f35e67e96f16a0328559111c597856c660f177c2
22 | `);
23 |
24 | return res.status(500).send("server error");
25 | }
26 |
27 | const user: any = jwt.verify(refreshToken, refreshTokenSecret);
28 |
29 | const userPlainObj = {
30 | id: user.id,
31 | name: user.name,
32 | phone: user.phone,
33 | email: user.email,
34 | admin: user.admin,
35 | };
36 |
37 | const accessToken = jwt.sign(userPlainObj, accessTokenSecret, {
38 | expiresIn: 60, // 10 minutes
39 | });
40 |
41 | const newRefreshToken = jwt.sign(userPlainObj, refreshTokenSecret, {
42 | expiresIn: "7d",
43 | });
44 |
45 | res.cookie("refresh", newRefreshToken, {
46 | secure: true,
47 | httpOnly: true,
48 | expires: dayjs().add(7, "days").toDate(),
49 | });
50 |
51 | return res.status(200).send({ user: userPlainObj, accessToken });
52 | } catch (error) {
53 | return res.status(400).send(error);
54 | }
55 | };
56 |
--------------------------------------------------------------------------------
/backend/src/controllers/polls/vote.ts:
--------------------------------------------------------------------------------
1 | import { Request, Response } from "express";
2 | import ElectionContract, { web3 } from "../../web3";
3 | // import memoryCache from "memory-cache";
4 | import * as yup from "yup";
5 |
6 | const checkSchema = yup.object({
7 | body: yup.object({
8 | id: yup.string().required(),
9 | }),
10 | });
11 |
12 | export const checkVoteability = async (req: Request, res: Response) => {
13 | try {
14 | await checkSchema.validate(req);
15 | } catch (error) {
16 | return res.status(400).send({ error });
17 | }
18 |
19 | const instance = await ElectionContract.deployed();
20 | const voters: Array = await instance.getVoters();
21 | const status: "not-started" | "running" | "finished" =
22 | await instance.getStatus();
23 |
24 | if (status !== "running") return res.status(400).send("election not running");
25 | if (voters.includes(req.body.id)) return res.send("already-voted");
26 |
27 | return res.send("not-voted");
28 | };
29 |
30 | const schema = yup.object({
31 | body: yup.object({
32 | id: yup.string().required(),
33 | name: yup.string().min(3).required(),
34 | candidate: yup.string().min(3).required(),
35 | }),
36 | });
37 |
38 | export default async (req: Request, res: Response) => {
39 | try {
40 | await schema.validate(req);
41 | } catch (error: any) {
42 | return res.status(400).send(error.errors);
43 | }
44 |
45 | const accounts = await web3.eth.getAccounts();
46 | const instance = await ElectionContract.deployed();
47 | const voters: Array = await instance.getVoters();
48 | const candidates: Array = await instance.getCandidates();
49 |
50 | if (voters.includes(req.body.id))
51 | return res.status(400).send("already voted");
52 |
53 | if (!candidates.includes(req.body.candidate))
54 | return res.status(400).send("no such candidate");
55 |
56 | await instance.vote(req.body.id, req.body.name, req.body.candidate, {
57 | from: accounts[0],
58 | });
59 |
60 | return res.send("successful");
61 | };
62 |
--------------------------------------------------------------------------------
/frontend/src/pages/Admin/Users.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from "react";
2 | import axios from "../../axios";
3 |
4 | type User = {
5 | id: number;
6 | name: string;
7 | citizenshipNumber: string;
8 | email: string;
9 | };
10 |
11 | const Users = () => {
12 | const [users, setUser] = useState([]);
13 |
14 | useEffect(() => {
15 | axios
16 | .get("/users/all")
17 | .then((res) => setUser(res.data.users))
18 | .catch((error) => console.log({ error }));
19 | }, []);
20 |
21 | const verifyUser = (id: number | string) => {
22 | axios
23 | .post("/users/verify", { userId: id })
24 | .then((res) => {
25 | console.log(res);
26 | removeUserFromList(id);
27 | })
28 | .catch((error) => console.log({ error }));
29 | };
30 |
31 | const deleteUser = (id: number | string) => {
32 | axios
33 | .delete(`/users/delete/${id}`)
34 | .then((res) => {
35 | console.log(res);
36 | removeUserFromList(id);
37 | })
38 | .catch((error) => console.log({ error }));
39 | };
40 |
41 | const removeUserFromList = (id: number | string) => {
42 | const index = users.findIndex((user) => user.id == id);
43 | const newList = [...users];
44 | newList.splice(index, 1);
45 | setUser(newList);
46 | };
47 |
48 | if (users.length === 0) return
;
49 |
50 | return (
51 |
52 | {users.map((user, index) => (
53 |
54 | {user.name}
55 |
56 |
57 | verifyUser(user.id)}
59 | className="button-primary"
60 | >
61 | verify
62 |
63 |
64 | deleteUser(user.id)}
66 | className="button-black"
67 | >
68 | delete
69 |
70 |
71 |
72 | ))}
73 |
74 | );
75 | };
76 |
77 | export default Users;
78 |
--------------------------------------------------------------------------------
/frontend/src/pages/User/Polls.tsx:
--------------------------------------------------------------------------------
1 | import React, { useContext, useEffect, useState } from "react";
2 | import axios from "../../axios";
3 | import Chart from "../../components/Polls/Chart";
4 | import Finished from "../../components/Polls/Finished";
5 | import Panel from "../../components/Polls/Panel";
6 | import Running from "../../components/Polls/Running";
7 | import Waiting from "../../components/Waiting";
8 | import { AuthContext } from "../../contexts/Auth";
9 |
10 | const User = () => {
11 | const [voteState, setVoteStatus] = useState<
12 | "finished" | "running" | "not-started" | "checking"
13 | >("checking");
14 | const [loading, setLoading] = useState(true);
15 | const [data, setData] = useState({ name: "", description: "", votes: {} });
16 | const [votable, setVotable] = useState("");
17 |
18 | const authContext = useContext(AuthContext);
19 |
20 | useEffect(() => {
21 | console.log("called here ?");
22 |
23 | axios
24 | .get("/polls/status")
25 | .then((res) => {
26 | setVoteStatus(res.data.status);
27 | setLoading(false);
28 | })
29 | .catch((error) => console.log({ error }));
30 | }, []);
31 |
32 | useEffect(() => {
33 | if (voteState !== "checking") {
34 | axios.get("/polls/").then((res) => {
35 | setData(res.data);
36 | console.log(res);
37 | setLoading(false);
38 | });
39 |
40 | axios
41 | .post("/polls/check-voteability", {
42 | id: authContext.id.toString(),
43 | })
44 | .then((res) => {
45 | setVotable(res.data);
46 | })
47 | .catch((err) => console.log(err));
48 | }
49 | });
50 |
51 | if (loading || voteState === "checking") return
;
52 |
53 | if (voteState === "not-started") return ;
54 |
55 | return (
56 |
57 | <>
58 | {voteState === "running" ? : }
59 |
60 |
66 | >
67 |
68 | );
69 | };
70 |
71 | export default User;
72 |
--------------------------------------------------------------------------------
/frontend/README.md:
--------------------------------------------------------------------------------
1 | # Getting Started with Create React App
2 |
3 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
4 |
5 | ## Available Scripts
6 |
7 | In the project directory, you can run:
8 |
9 | ### `npm start`
10 |
11 | Runs the app in the development mode.\
12 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
13 |
14 | The page will reload if you make edits.\
15 | You will also see any lint errors in the console.
16 |
17 | ### `npm test`
18 |
19 | Launches the test runner in the interactive watch mode.\
20 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
21 |
22 | ### `npm run build`
23 |
24 | Builds the app for production to the `build` folder.\
25 | It correctly bundles React in production mode and optimizes the build for the best performance.
26 |
27 | The build is minified and the filenames include the hashes.\
28 | Your app is ready to be deployed!
29 |
30 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
31 |
32 | ### `npm run eject`
33 |
34 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!**
35 |
36 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
37 |
38 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own.
39 |
40 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it.
41 |
42 | ## Learn More
43 |
44 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
45 |
46 | To learn React, check out the [React documentation](https://reactjs.org/).
47 |
--------------------------------------------------------------------------------
/frontend/src/components/CustomRoutes.tsx:
--------------------------------------------------------------------------------
1 | import React, { useContext } from "react";
2 | import { Routes, Route } from "react-router-dom";
3 | import Home from "../pages/Home";
4 | import Login from "../pages/Login";
5 | import Signup from "../pages/Signup";
6 | import View from "../pages/View";
7 | import { AuthContext } from "../contexts/Auth";
8 | import UserPollsPage from "../pages/User/Polls";
9 | import HomePage from "../pages/Admin/Home";
10 | import ProfilePage from "../pages/User/Profile";
11 | import Default from "../layouts/Default";
12 | import AdminUsersPage from "../pages/Admin/Users";
13 | import AdminVerifyPage from "../pages/Admin/Verify";
14 |
15 | export default () => {
16 | const authContext = useContext(AuthContext);
17 |
18 | const getRoutes = (): JSX.Element => {
19 | if (authContext.loading) return loading...
;
20 |
21 | if (authContext.authenticated) {
22 | // if the user is authenticated then
23 |
24 | const adminMenu = [
25 | { name: "Home", link: "/" },
26 | { name: "Verify Users", link: "/users" },
27 | { name: "Profile", link: "/profile" },
28 | ];
29 |
30 | const userMenu = [
31 | { name: "Polls", link: "/" },
32 | { name: "Profile", link: "/profile" },
33 | ];
34 |
35 | if (authContext.isAdmin) {
36 | // if the user is admin
37 | return (
38 |
39 |
40 | } />
41 | } />
42 | } />
43 | } />
44 |
45 |
46 | );
47 | } else {
48 | // if the user in not admin
49 | return (
50 |
51 |
52 | } />
53 | } />
54 |
55 |
56 | );
57 | }
58 | } else {
59 | // if the user is not authenticated
60 | return (
61 |
62 | } />
63 | } />
64 | } />
65 | } />
66 |
67 | );
68 | }
69 | };
70 |
71 | return getRoutes();
72 | };
73 |
--------------------------------------------------------------------------------
/frontend/src/layouts/Default.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from "react";
2 | import { useNavigate } from "react-router";
3 | import { useLocation } from "react-router-dom";
4 |
5 | type MenuLink = {
6 | name: string;
7 | link: string;
8 | };
9 |
10 | type DefaultProps = {
11 | menu: MenuLink[];
12 | children: JSX.Element;
13 | };
14 |
15 | const Default = (props: DefaultProps) => {
16 | const navigate = useNavigate();
17 | const { pathname } = useLocation();
18 |
19 | useEffect(() => {
20 | document.getElementById("default-sidebar")?.classList.add("hide");
21 | document.getElementById("default-sidebar")?.classList.remove("display");
22 |
23 | const hideIfOutside = (e: any) => {
24 | const sidebar = document.getElementById("default-sidebar");
25 | const outsideHam = document.getElementById("outside-ham");
26 |
27 | if (!sidebar?.contains(e.target) && !outsideHam?.contains(e.target)) {
28 | if (!sidebar?.classList.contains("hide")) {
29 | sidebar?.classList.add("hide");
30 | sidebar?.classList.remove("display");
31 | }
32 | }
33 | };
34 |
35 | window.addEventListener("click", hideIfOutside);
36 |
37 | return () => {
38 | window.removeEventListener("click", hideIfOutside);
39 | };
40 | }, []);
41 |
42 | const toggleHandler = () => {
43 | document.getElementById("default-sidebar")?.classList.toggle("hide");
44 | document.getElementById("default-sidebar")?.classList.toggle("display");
45 | };
46 |
47 | return (
48 |
49 |
50 |
51 |
52 |
53 |
54 |
74 |
75 |
76 |
{props.children}
77 |
78 | );
79 | };
80 |
81 | export default Default;
82 |
--------------------------------------------------------------------------------
/frontend/src/components/Home/Features.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import Feature from "../Features/Feature";
3 | import { MdGppGood, MdLibraryAddCheck, MdLock, MdShare } from "react-icons/md";
4 |
5 | const Features = () => {
6 | return (
7 |
8 |
Amazing Features
9 |
10 | lorem ipsum dosa is posa and gosa is the best thing i can come up with
11 |
12 |
13 |
14 |
15 |
} align="right">
16 |
17 | Immutability means something that can’t be changed or altered.
18 | This is one of the top blockchain features that help to ensure
19 | that the technology will remain as it is, a permanent, unalterable
20 | network.
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
} align="left">
31 |
32 | Immutability means something that can’t be changed or altered.
33 | This is one of the top blockchain features that help to ensure
34 | that the technology will remain as it is, a permanent, unalterable
35 | network.
36 |
37 |
38 |
39 |
40 |
} align="right">
41 |
42 | Immutability means something that can’t be changed or altered.
43 | This is one of the top blockchain features that help to ensure
44 | that the technology will remain as it is, a permanent, unalterable
45 | network.
46 |
47 |
48 |
49 |
50 |
}
53 | align="left"
54 | >
55 |
56 | Immutability means something that can’t be changed or altered.
57 | This is one of the top blockchain features that help to ensure
58 | that the technology will remain as it is, a permanent, unalterable
59 | network.
60 |
61 |
62 |
63 |
64 |
65 | );
66 | };
67 |
68 | export default Features;
69 |
--------------------------------------------------------------------------------
/backend/src/controllers/auth/login.ts:
--------------------------------------------------------------------------------
1 | import { Request, Response } from "express";
2 | import * as yup from "yup";
3 | import { User } from "../../entity/User";
4 | import bcrypt from "bcrypt";
5 | import jwt from "jsonwebtoken";
6 | import dayjs from "dayjs";
7 |
8 | const schema = yup.object({
9 | body: yup.object({
10 | email: yup.string().email().required(),
11 | password: yup.string().min(3).required(),
12 | }),
13 | });
14 |
15 | export default async (req: Request, res: Response) => {
16 | let user = null;
17 |
18 | // throws error when the POST-ed queries are invalide (email and password)
19 | try {
20 | await schema.validate(req);
21 | } catch (error: any) {
22 | return res.status(400).send(error.errors);
23 | }
24 |
25 | // throws error if user with provided email not found
26 | try {
27 | user = await User.findOneOrFail({ email: req.body.email });
28 | } catch (error: any) {
29 | return res.status(404).send(error);
30 | }
31 |
32 | if (!user.verified) return res.status(400).send("Not verified");
33 |
34 | const match = await bcrypt.compare(req.body.password, user.password);
35 | //exits if password doesn't match
36 | if (!match) return res.status(400).send("password doesn't match");
37 |
38 | // if the code reaches here then the user is authenticated
39 | // hurray :D
40 |
41 | const accessTokenSecret = process.env.ACCESS_TOKEN_SECRET;
42 | const refreshTokenSecret = process.env.REFRESH_TOKEN_SECRET;
43 |
44 | if (!accessTokenSecret || !refreshTokenSecret) {
45 | console.log("you forgot to add .env file to the project?");
46 | console.log(`
47 | add the following:
48 |
49 | ACCESS_TOKEN_SECRET=976a66a5bd23b2050019f380c4decbbefdf8ff91cf502c68a3fe1ced91d7448cc54ce6c847657d53294e40889cef5bd996ec5b0fefc1f56270e06990657eeb6e
50 |
51 | REFRESH_TOKEN_SECRET=5f567afa6406225c4a759daae77e07146eca5df8149353a844fa9ab67fba22780cb4baa5ea508214934531a6f35e67e96f16a0328559111c597856c660f177c2
52 | `);
53 |
54 | return res.status(500).send("server error");
55 | }
56 |
57 | const plainUserObject = {
58 | id: user.id,
59 | name: user.name,
60 | citizenshipNumber: user.citizenshipNumber,
61 | email: user.email,
62 | admin: user.admin,
63 | };
64 | const accessToken = jwt.sign(plainUserObject, accessTokenSecret, {
65 | expiresIn: 60,
66 | });
67 | const refreshToken = jwt.sign(plainUserObject, refreshTokenSecret, {
68 | expiresIn: "7d",
69 | });
70 |
71 | res.cookie("refreshToken", refreshToken, {
72 | expires: dayjs().add(7, "days").toDate(),
73 | });
74 |
75 | return res.send({ user, accessToken });
76 | };
77 |
--------------------------------------------------------------------------------
/frontend/src/contexts/Auth.tsx:
--------------------------------------------------------------------------------
1 | import React, { createContext, useEffect, useState } from "react";
2 | import { useNavigate } from "react-router";
3 | import axios from "../axios";
4 |
5 | type ContextProps = {
6 | children: JSX.Element;
7 | };
8 |
9 | type User = {
10 | id: number;
11 | name: string;
12 | admin: boolean;
13 | };
14 |
15 | export const AuthContext = createContext({
16 | id: 0,
17 | name: "",
18 | isAdmin: false,
19 | authenticated: false,
20 | accessToken: "",
21 | loading: true,
22 | authenticate: (user: User, token: string) => {},
23 | logout: () => {},
24 | });
25 |
26 | export default (props: ContextProps): JSX.Element => {
27 | const navigate = useNavigate();
28 |
29 | const [authentication, setAuthentication] = useState({
30 | id: 0,
31 | name: "",
32 | isAdmin: false,
33 | authenticated: false,
34 | accessToken: "",
35 | loading: true,
36 | });
37 |
38 | const checkAuthentication = () => {
39 | axios
40 | .post("/auth/check")
41 | .then((res) => authenticate(res.data.user, res.data.accessToken, false))
42 | .catch((error) => {
43 | console.log(error);
44 | setAuthentication({ ...authentication, loading: false });
45 | });
46 | };
47 |
48 | useEffect(() => {
49 | checkAuthentication();
50 |
51 | const interval = setInterval(checkAuthentication, 5 * 1000);
52 |
53 | return () => clearInterval(interval);
54 | }, []);
55 |
56 | const authenticate = (
57 | user: User,
58 | token: string,
59 | redirect: boolean = true
60 | ) => {
61 | setAuthentication({
62 | id: user.id,
63 | name: user.name,
64 | isAdmin: user.admin,
65 | authenticated: true,
66 | accessToken: token,
67 | loading: false,
68 | });
69 |
70 | if (redirect) navigate("/");
71 | };
72 |
73 | const logout = async () => {
74 | await axios.post("/auth/logout");
75 |
76 | setAuthentication({
77 | id: 0,
78 | name: "",
79 | isAdmin: false,
80 | authenticated: false,
81 | accessToken: "",
82 | loading: false,
83 | });
84 |
85 | navigate("/");
86 | };
87 |
88 | return (
89 |
101 | {props.children}
102 |
103 | );
104 | };
105 |
--------------------------------------------------------------------------------
/frontend/src/components/Polls/Chart.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import axios from "../../axios";
3 |
4 | interface ChartProps {
5 | votes: any;
6 | enableVote?: boolean;
7 | userId?: number;
8 | userName?: string;
9 | }
10 |
11 | const Chart = (props: ChartProps) => {
12 | const votes = props.votes;
13 |
14 | const getButtons = () => {
15 | const names = [];
16 |
17 | const vote = (candidate: string) => {
18 | axios
19 | .post("/polls/vote", {
20 | id: props.userId?.toString(),
21 | name: props.userName,
22 | candidate,
23 | })
24 | .then((_) => window.location.reload())
25 | .catch((err) => console.log({ err }));
26 | };
27 |
28 | for (const name in votes) {
29 | names.push(
30 | vote(name)}
32 | key={name}
33 | className="button-wrapper text-normal"
34 | >
35 | vote
36 |
37 | );
38 | }
39 |
40 | return names;
41 | };
42 |
43 | const getNames = () => {
44 | const names = [];
45 |
46 | for (const name in votes) {
47 | names.push(
48 |
49 | {name}
50 |
51 | );
52 | }
53 |
54 | return names;
55 | };
56 |
57 | const getTotal = () => {
58 | let total = 0;
59 |
60 | for (const name in votes) {
61 | total += parseInt(votes[name]);
62 | }
63 |
64 | return total;
65 | };
66 |
67 | const getBars = () => {
68 | const bars = [];
69 | const total = getTotal();
70 |
71 | for (const name in votes) {
72 | const count = votes[name];
73 | bars.push(
74 |
75 |
89 | {votes[name]}
90 |
91 |
92 | );
93 | }
94 |
95 | return bars;
96 | };
97 |
98 | return (
99 |
100 |
{getBars()}
101 |
{getNames()}
102 |
103 | {props.enableVote ? (
104 |
{getButtons()}
105 | ) : null}
106 |
107 | );
108 | };
109 |
110 | export default Chart;
111 |
--------------------------------------------------------------------------------
/frontend/src/styles/index.scss:
--------------------------------------------------------------------------------
1 | @import url("https://fonts.googleapis.com/css2?family=Josefin+Sans:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;1,100;1,200;1,300;1,400;1,500;1,600;1,700&family=Ubuntu:ital,wght@0,300;0,400;0,500;0,700;1,300;1,400;1,500;1,700&display=swap");
2 | @import url("https://fonts.googleapis.com/css2?family=Inter:wght@100;200;300;400;500;600;700;800;900&display=swap");
3 | @import "~bootstrap-icons/font/bootstrap-icons.css";
4 |
5 | $theme-gray: #f6f6f6;
6 | $theme-black: #333333;
7 | $theme-primary: #4daaa7;
8 | $theme-secondary: #3f8f8b;
9 |
10 | @mixin box-shadow {
11 | box-shadow: 2px 2px 10px rgba(50, 50, 50, 0.1);
12 | }
13 |
14 | body,
15 | html {
16 | position: relative;
17 | margin: 0;
18 | padding: 0;
19 | // font-family: "Inter", sans-serif;
20 | font-family: "Josefin Sans", sans-serif;
21 | -webkit-font-smoothing: antialiased;
22 | -moz-osx-font-smoothing: grayscale;
23 | color: $theme-black;
24 | }
25 |
26 | code {
27 | font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
28 | monospace;
29 | }
30 |
31 | button {
32 | border: none;
33 | outline: none;
34 | border-radius: 5px;
35 | padding: 10px;
36 | margin-right: 5px;
37 | cursor: pointer;
38 | transition: 0.3s;
39 | }
40 |
41 | .button-black {
42 | background-color: $theme-black;
43 | color: white;
44 | }
45 | .button-primary {
46 | background-color: $theme-primary;
47 | color: white;
48 | }
49 |
50 | .button-secondary {
51 | background-color: $theme-secondary;
52 | color: white;
53 | }
54 |
55 | button,
56 | .title-large,
57 | .title-small {
58 | text-transform: uppercase;
59 | }
60 |
61 | .title-large {
62 | font-size: 2.8rem;
63 | font-weight: 900;
64 | }
65 |
66 | .title-small {
67 | font-size: 1.6rem;
68 | font-weight: 300;
69 | }
70 |
71 | .text-normal {
72 | font-weight: 300;
73 | }
74 |
75 | .success-message,
76 | .error-message {
77 | box-sizing: border-box;
78 | width: 400px;
79 | padding: 20px 30px 20px 30px;
80 | border-radius: 10px;
81 | margin-bottom: 50px;
82 | color: white;
83 |
84 | span {
85 | margin-right: 10px;
86 | }
87 | }
88 |
89 | .success-message {
90 | background-color: $theme-primary;
91 | }
92 |
93 | .error-message {
94 | background-color: red;
95 | }
96 |
97 | @import "./layouts/Boxes.scss";
98 |
99 | @import "./form.scss";
100 | @import "./vote-status.scss";
101 | @import "./pages/View.scss";
102 | @import "./pages/Landing.scss";
103 | @import "./pages/Features.scss";
104 | @import "./pages/Admin/Create.scss";
105 | @import "./pages/Admin/Polls.scss";
106 | @import "./pages/User/Profile.scss";
107 | @import "./layouts/Login.scss";
108 | @import "./layouts/Default.scss";
109 | @import "./layouts/Users.scss";
110 | @import "./layouts/Polls.scss";
111 | @import "./components/Back.scss";
112 | @import "./components/Waiting.scss";
113 | @import "./components/Features/Feature.scss";
114 | @import "./components/Polls/Chart.scss";
115 | @import "./components/Polls/Panel.scss";
116 |
--------------------------------------------------------------------------------
/frontend/src/pages/Login.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useContext } from "react";
2 | import { useNavigate } from "react-router";
3 | import { Formik } from "formik";
4 | import { RouteProps } from "react-router";
5 | import LoginLayout from "../layouts/Login";
6 | import * as Yup from "yup";
7 | import axios from "../axios";
8 | import { AuthContext } from "../contexts/Auth";
9 |
10 | const schema = Yup.object().shape({
11 | email: Yup.string().email("Invalid email").required("Required"),
12 | password: Yup.string().min(3).required("Required"),
13 | });
14 |
15 | const Login = (props: RouteProps): JSX.Element => {
16 | const navigate = useNavigate();
17 | const authContext = useContext(AuthContext);
18 |
19 | const [error, setError] = useState("");
20 |
21 | return (
22 |
23 |
24 |
25 |
{
32 | axios
33 | .post("/auth/login", { ...values })
34 | .then((res) => {
35 | authContext.authenticate(res.data.user, res.data.accessToken);
36 | })
37 | .catch((err) => {
38 | let error = err.message;
39 | if (err?.response?.data)
40 | error = JSON.stringify(err.response.data);
41 | setError(error);
42 | });
43 | }}
44 | >
45 | {({ errors, touched, getFieldProps, handleSubmit }) => (
46 |
77 | )}
78 |
79 |
80 |
Forgot Password?
81 |
82 |
83 |
84 |
navigate("/signup")}
86 | className="button-secondary"
87 | >
88 | Create a New Account
89 |
90 |
91 |
92 |
93 | );
94 | };
95 |
96 | export default Login;
97 |
--------------------------------------------------------------------------------
/backend/contracts/Election.sol:
--------------------------------------------------------------------------------
1 | //SPDX-License-Identifier: UNLICENSED
2 | pragma solidity >=0.4.22 <0.9.0;
3 |
4 | contract Election {
5 | mapping(address => bool) admins;
6 | string name; // name of the election. example: for president
7 | string description; // description of the election
8 | bool started;
9 | bool ended;
10 |
11 | constructor() {
12 | admins[msg.sender] = true;
13 | started = false;
14 | ended = false;
15 | }
16 |
17 | modifier onlyAdmin() {
18 | //require(admins[msg.sender] == true, "Only Admin");
19 | _;
20 | }
21 |
22 | function addAdmin(address _address) public onlyAdmin {
23 | admins[_address] = true;
24 | }
25 |
26 | /*****************************CANDIDATES SECTION*****************************/
27 |
28 | struct Candidate {
29 | string name;
30 | string info;
31 | bool exists;
32 | }
33 | mapping(string => Candidate) public candidates;
34 | string[] candidateNames;
35 |
36 | function addCandidate(string memory _candidateName, string memory _info)
37 | public
38 | onlyAdmin
39 | {
40 | Candidate memory newCandidate = Candidate({
41 | name: _candidateName,
42 | info: _info,
43 | exists: true
44 | });
45 |
46 | candidates[_candidateName] = newCandidate;
47 | candidateNames.push(_candidateName);
48 | }
49 |
50 | function getCandidates() public view returns (string[] memory) {
51 | return candidateNames;
52 | }
53 |
54 | /*****************************CANDIDATES SECTION*****************************/
55 |
56 | /*****************************ELECTION SECTION*****************************/
57 |
58 | function setElectionDetails(string memory _name, string memory _description)
59 | public
60 | onlyAdmin
61 | {
62 | name = _name;
63 | description = _description;
64 | started = true;
65 | ended = false;
66 | }
67 |
68 | function getElectionName() public view returns (string memory) {
69 | return name;
70 | }
71 |
72 | function getElectionDescription() public view returns (string memory) {
73 | return description;
74 | }
75 |
76 | function getTotalCandidates() public view returns (uint256) {
77 | return candidateNames.length;
78 | }
79 |
80 | /*****************************ELECTION SECTION*****************************/
81 |
82 | /*****************************VOTER SECTION*****************************/
83 |
84 | struct Vote {
85 | address voterAddress;
86 | string voterId;
87 | string voterName;
88 | string candidate;
89 | }
90 | Vote[] votes;
91 | mapping(string => bool) public voterIds;
92 | string[] votersArray;
93 |
94 | function vote(
95 | string memory _voterId,
96 | string memory _voterName,
97 | string memory _candidateName
98 | ) public {
99 | require(started == true && ended == false);
100 | require(candidates[_candidateName].exists, "No such candidate");
101 | require(!voterIds[_voterId], "Already Voted");
102 |
103 | Vote memory newVote = Vote({
104 | voterAddress: msg.sender,
105 | voterId: _voterId,
106 | voterName: _voterName,
107 | candidate: _candidateName
108 | });
109 |
110 | votes.push(newVote);
111 | voterIds[_voterId] = true;
112 | votersArray.push(_voterId);
113 | }
114 |
115 | function getVoters() public view returns (string[] memory) {
116 | return votersArray;
117 | }
118 |
119 | /*****************************VOTER SECTION*****************************/
120 |
121 | function getVotes() public view onlyAdmin returns (Vote[] memory) {
122 | return votes;
123 | }
124 |
125 | function getTotalVoter() public view returns (uint256) {
126 | return votersArray.length;
127 | }
128 |
129 | function endElection() public onlyAdmin {
130 | require(started == true && ended == false);
131 |
132 | started = true;
133 | ended = true;
134 | }
135 |
136 | function resetElection() public onlyAdmin {
137 | require(started == true && ended == true);
138 |
139 | for (uint32 i = 0; i < candidateNames.length; i++) {
140 | delete candidates[candidateNames[i]];
141 | }
142 |
143 | for (uint32 i = 0; i < votersArray.length; i++) {
144 | delete voterIds[votersArray[i]];
145 | }
146 |
147 | name = "";
148 | description = "";
149 |
150 | delete votes;
151 | delete candidateNames;
152 | delete votersArray;
153 |
154 | started = false;
155 | ended = false;
156 | }
157 |
158 | function getStatus() public view returns (string memory) {
159 | if (started == true && ended == true) {
160 | return "finished";
161 | }
162 |
163 | if (started == true && ended == false) {
164 | return "running";
165 | }
166 |
167 | return "not-started";
168 | }
169 | }
170 |
--------------------------------------------------------------------------------
/backend/truffle-config.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Use this file to configure your truffle project. It's seeded with some
3 | * common settings for different networks and features like migrations,
4 | * compilation and testing. Uncomment the ones you need or modify
5 | * them to suit your project as necessary.
6 | *
7 | * More information about configuration can be found at:
8 | *
9 | * trufflesuite.com/docs/advanced/configuration
10 | *
11 | * To deploy via Infura you'll need a wallet provider (like @truffle/hdwallet-provider)
12 | * to sign your transactions before they're sent to a remote public node. Infura accounts
13 | * are available for free at: infura.io/register.
14 | *
15 | * You'll also need a mnemonic - the twelve word phrase the wallet uses to generate
16 | * public/private key pairs. If you're publishing your code to GitHub make sure you load this
17 | * phrase from a file you've .gitignored so it doesn't accidentally become public.
18 | *
19 | */
20 |
21 | // const HDWalletProvider = require('@truffle/hdwallet-provider');
22 | //
23 | // const fs = require('fs');
24 | // const mnemonic = fs.readFileSync(".secret").toString().trim();
25 |
26 | module.exports = {
27 | /**
28 | * Networks define how you connect to your ethereum client and let you set the
29 | * defaults web3 uses to send transactions. If you don't specify one truffle
30 | * will spin up a development blockchain for you on port 9545 when you
31 | * run `develop` or `test`. You can ask a truffle command to use a specific
32 | * network from the command line, e.g
33 | *
34 | * $ truffle test --network
35 | */
36 |
37 | networks: {
38 | // Useful for testing. The `development` name is special - truffle uses it by default
39 | // if it's defined here and no other network is specified at the command line.
40 | // You should run a client (like ganache-cli, geth or parity) in a separate terminal
41 | // tab if you use this network and you must also set the `host`, `port` and `network_id`
42 | // options below to some value.
43 | //
44 | development: {
45 | host: "127.0.0.1", // Localhost (default: none)
46 | port: 7545, // Standard Ethereum port (default: none)
47 | network_id: "*", // Any network (default: none)
48 | // from: "0xa5Bcae61a3a8353C8532c0BcC21e2f6AEF1659E6",
49 | },
50 | // Another network with more advanced options...
51 | // advanced: {
52 | // port: 8777, // Custom port
53 | // network_id: 1342, // Custom network
54 | // gas: 8500000, // Gas sent with each transaction (default: ~6700000)
55 | // gasPrice: 20000000000, // 20 gwei (in wei) (default: 100 gwei)
56 | // from: , // Account to send txs from (default: accounts[0])
57 | // websocket: true // Enable EventEmitter interface for web3 (default: false)
58 | // },
59 | // Useful for deploying to a public network.
60 | // NB: It's important to wrap the provider as a function.
61 | // ropsten: {
62 | // provider: () => new HDWalletProvider(mnemonic, `https://ropsten.infura.io/v3/YOUR-PROJECT-ID`),
63 | // network_id: 3, // Ropsten's id
64 | // gas: 5500000, // Ropsten has a lower block limit than mainnet
65 | // confirmations: 2, // # of confs to wait between deployments. (default: 0)
66 | // timeoutBlocks: 200, // # of blocks before a deployment times out (minimum/default: 50)
67 | // skipDryRun: true // Skip dry run before migrations? (default: false for public nets )
68 | // },
69 | // Useful for private networks
70 | // private: {
71 | // provider: () => new HDWalletProvider(mnemonic, `https://network.io`),
72 | // network_id: 2111, // This network is yours, in the cloud.
73 | // production: true // Treats this network as if it was a public net. (default: false)
74 | // }
75 | },
76 |
77 | // Set default mocha options here, use special reporters etc.
78 | mocha: {
79 | // timeout: 100000
80 | },
81 |
82 | // Configure your compilers
83 | compilers: {
84 | solc: {
85 | version: "0.8.11", // Fetch exact version from solc-bin (default: truffle's version)
86 | // docker: true, // Use "0.5.1" you've installed locally with docker (default: false)
87 | // settings: { // See the solidity docs for advice about optimization and evmVersion
88 | // optimizer: {
89 | // enabled: false,
90 | // runs: 200
91 | // },
92 | // evmVersion: "byzantium"
93 | // }
94 | },
95 | },
96 |
97 | // Truffle DB is currently disabled by default; to enable it, change enabled:
98 | // false to enabled: true. The default storage location can also be
99 | // overridden by specifying the adapter settings, as shown in the commented code below.
100 | //
101 | // NOTE: It is not possible to migrate your contracts to truffle DB and you should
102 | // make a backup of your artifacts to a safe location before enabling this feature.
103 | //
104 | // After you backed up your artifacts you can utilize db by running migrate as follows:
105 | // $ truffle migrate --reset --compile-all
106 | //
107 | // db: {
108 | // enabled: false,
109 | // host: "127.0.0.1",
110 | // adapter: {
111 | // name: "sqlite",
112 | // settings: {
113 | // directory: ".db"
114 | // }
115 | // }
116 | // }
117 | };
118 |
--------------------------------------------------------------------------------
/frontend/src/pages/Signup.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import { useNavigate } from "react-router";
3 | import { Formik } from "formik";
4 | import LoginLayout from "../layouts/Login";
5 | import * as Yup from "yup";
6 | import axios from "../axios";
7 |
8 | const schema = Yup.object().shape({
9 | name: Yup.string().min(3).required(),
10 | email: Yup.string().email("Invalid email").required("Required"),
11 | citizenshipNumber: Yup.string().min(4).required(),
12 | password: Yup.string().min(3).required("Required"),
13 | confirm: Yup.string()
14 | .oneOf([Yup.ref("password")], "must be same as password")
15 | .required(),
16 | });
17 |
18 | const Signup = (): JSX.Element => {
19 | const navigate = useNavigate();
20 |
21 | const [error, setError] = useState("");
22 | const [success, setSuccess] = useState("");
23 |
24 | return (
25 |
26 |
27 |
28 |
{
38 | axios
39 | .post("/auth/signup", {
40 | name,
41 | email,
42 | citizenshipNumber,
43 | password,
44 | })
45 | .then((res) => {
46 | setError("");
47 | setSuccess("Signup Successful!");
48 | })
49 | .catch((err) => {
50 | let error: string = err.message;
51 | if (err?.response?.data)
52 | error = JSON.stringify(err.response.data);
53 | setError(error.slice(0, 50));
54 | });
55 | }}
56 | >
57 | {({ errors, touched, getFieldProps, handleSubmit }) => (
58 |
127 | )}
128 |
129 |
130 |
131 |
Already have an account?
132 |
133 |
navigate("/login")}
135 | className="button-secondary"
136 | type="button"
137 | >
138 | Login
139 |
140 |
141 |
142 |
143 | );
144 | };
145 |
146 | export default Signup;
147 |
--------------------------------------------------------------------------------
/frontend/src/pages/Admin/Start.tsx:
--------------------------------------------------------------------------------
1 | import React, { useRef, useState } from "react";
2 | import { Formik } from "formik";
3 | import axios from "../../axios";
4 | import * as yup from "yup";
5 |
6 | const schema = yup.object({
7 | name: yup.string().min(3).required(),
8 | description: yup.string().min(10).required(),
9 | });
10 |
11 | interface Candidate {
12 | name: string;
13 | info: string;
14 | }
15 |
16 | const Start = () => {
17 | const [candidates, setCandidates] = useState>([]);
18 | const [loading, setLoading] = useState(false);
19 | const [error, setError] = useState("");
20 | const [name, setName] = useState("");
21 | const [info, setInfo] = useState("");
22 |
23 | const candidateField = useRef(null);
24 | const candidateInfoField = useRef(null);
25 |
26 | return (
27 |
28 | {error !== "" ?
{error}
: null}
29 |
30 |
{
37 | setLoading(true);
38 |
39 | let candidatesError = "";
40 |
41 | if (candidates.length < 2) candidatesError = "Not Enough Candidates";
42 |
43 | for (let i = 0; i < candidates.length; i++) {
44 | const candidate = candidates[i];
45 |
46 | if (candidate.name.length < 3) {
47 | candidatesError = "invalid name " + candidate.name;
48 | break;
49 | }
50 |
51 | if (candidate.info.length < 10) {
52 | candidatesError = "invalid info for " + candidate.name;
53 | break;
54 | }
55 | }
56 |
57 | setError(candidatesError);
58 |
59 | if (candidatesError === "") {
60 | axios
61 | .post("/polls/start", { name, description, candidates })
62 | .then((_) => {
63 | window.location.reload();
64 | })
65 | .catch((err) => {
66 | let error = err.message;
67 | if (err?.response?.data) error = err.response.data;
68 | setError(error.slice(0, 50));
69 | setLoading(false);
70 | });
71 | }
72 | }}
73 | >
74 | {({ errors, touched, getFieldProps, handleSubmit }) => (
75 |
171 | )}
172 |
173 |
174 | );
175 | };
176 |
177 | export default Start;
178 |
--------------------------------------------------------------------------------
/backend/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */
4 |
5 | /* Projects */
6 | // "incremental": true, /* Enable incremental compilation */
7 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
8 | // "tsBuildInfoFile": "./", /* Specify the folder for .tsbuildinfo incremental compilation files. */
9 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects */
10 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
11 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
12 |
13 | /* Language and Environment */
14 | "target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
15 | // "lib": ["es2019"], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
16 | // "jsx": "preserve", /* Specify what JSX code is generated. */
17 | "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */
18 | "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
19 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h' */
20 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
21 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using `jsx: react-jsx*`.` */
22 | // "reactNamespace": "", /* Specify the object invoked for `createElement`. This only applies when targeting `react` JSX emit. */
23 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
24 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
25 |
26 | /* Modules */
27 | "module": "commonjs", /* Specify what module code is generated. */
28 | // "rootDir": "./", /* Specify the root folder within your source files. */
29 | // "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */
30 | // "baseUrl": ".", /* Specify the base directory to resolve non-relative module names. */
31 | // "paths": {
32 | // "*": ["./@types/*"]
33 | // }, /* Specify a set of entries that re-map imports to additional lookup locations. */
34 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
35 | // "typeRoots": [], /* Specify multiple folders that act like `./node_modules/@types`. */
36 | // "types": [], /* Specify type package names to be included without being referenced in a source file. */
37 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
38 | "resolveJsonModule": true, /* Enable importing .json files */
39 | // "noResolve": true, /* Disallow `import`s, `require`s or ``s from expanding the number of files TypeScript should add to a project. */
40 |
41 | /* JavaScript Support */
42 | "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */
43 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
44 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from `node_modules`. Only applicable with `allowJs`. */
45 |
46 | /* Emit */
47 | "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
48 | "declarationMap": true, /* Create sourcemaps for d.ts files. */
49 | "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
50 | "sourceMap": true, /* Create source map files for emitted JavaScript files. */
51 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If `declaration` is true, also designates a file that bundles all .d.ts output. */
52 | "outDir": "./dist", /* Specify an output folder for all emitted files. */
53 | // "removeComments": true, /* Disable emitting comments. */
54 | // "noEmit": true, /* Disable emitting files from a compilation. */
55 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
56 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types */
57 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
58 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
59 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
60 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
61 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
62 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
63 | // "newLine": "crlf", /* Set the newline character for emitting files. */
64 | // "stripInternal": true, /* Disable emitting declarations that have `@internal` in their JSDoc comments. */
65 | // "noEmitHelpers": true, /* Disable generating custom helper functions like `__extends` in compiled output. */
66 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
67 | // "preserveConstEnums": true, /* Disable erasing `const enum` declarations in generated code. */
68 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */
69 | // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */
70 |
71 | /* Interop Constraints */
72 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
73 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
74 | "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */
75 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
76 | "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
77 |
78 | /* Type Checking */
79 | "strict": true, /* Enable all strict type-checking options. */
80 | // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied `any` type.. */
81 | // "strictNullChecks": true, /* When type checking, take into account `null` and `undefined`. */
82 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
83 | // "strictBindCallApply": true, /* Check that the arguments for `bind`, `call`, and `apply` methods match the original function. */
84 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
85 | // "noImplicitThis": true, /* Enable error reporting when `this` is given the type `any`. */
86 | // "useUnknownInCatchVariables": true, /* Type catch clause variables as 'unknown' instead of 'any'. */
87 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
88 | // "noUnusedLocals": true, /* Enable error reporting when a local variables aren't read. */
89 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read */
90 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
91 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
92 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
93 | // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */
94 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
95 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type */
96 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
97 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
98 |
99 | /* Completeness */
100 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
101 | "skipLibCheck": true /* Skip type checking all .d.ts files. */
102 | },
103 | }
104 |
--------------------------------------------------------------------------------