Compare commits

...

9 Commits

Author SHA1 Message Date
Adrian Gunnar Lauterer 408035c2eb fix: remove .vscode 2024-10-12 18:52:47 +02:00
Adrian Gunnar Lauterer ab616cb181
started work on db 2024-05-26 03:53:16 +02:00
Adrian Gunnar Lauterer 8593af2441 frontend init 2024-05-25 23:31:51 +02:00
Adrian Gunnar Lauterer 57e0cb6a88 Update README.md 2024-05-12 23:58:03 +02:00
Adrian Gunnar Lauterer f035de1145
added some html mocs of the website 2024-05-12 23:52:29 +02:00
Adrian Gunnar Lauterer b2a0f85563 Update openapi.yaml
Done to not require tokens for authorization (instead do it serverside) and move them to a namespace instead of a datetime range.
2024-05-12 02:31:44 +02:00
Adrian Gunnar Lauterer 0246f22e3e
added autogenerated rust server code 2024-05-11 23:30:37 +02:00
Adrian Gunnar Lauterer e3cc07061c added openapi.yaml
Signed-off-by: Adrian Gunnar Lauterer <adriangl@pvv.ntnu.no>
2024-05-11 22:51:55 +02:00
Adrian Gunnar Lauterer 1293fa0b87 Merge pull request 'README: add clarification for filetype, reformat' (#1) from readme-clarification into main
Reviewed-on: #1
2024-05-11 19:03:26 +02:00
31 changed files with 3769 additions and 995 deletions

4
.gitignore vendored
View File

@ -14,3 +14,7 @@ Cargo.lock
# MSVC Windows builds of rustc generate these, which store debugging information
*.pdb
.vscode

2733
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -6,15 +6,9 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
# for web server
rocket = { version = "0.5.0", features = ["json", "uuid"] }
rocket_dyn_templates = { version = "0.1.0", features = ["handlebars", "tera"] }
# for json serialization
serde = "1.0.196"
# for random number generation
rand = "0.8.5"
# for json serialization
serde_json = "1.0.113"
# for uuid generation
uuid = { version = "1.7.0", features = ["serde", "v4"] }
# for command line arguments
@ -23,5 +17,30 @@ structopt = "0.3.23"
csv = "1.1.6"
clap = { version = "4.3.24", features = ["derive"] }
stv-rs = "0.3.0"
stv-rs = "0.3.0"
serde = { version = "1.0", features = ["derive"] }
serde_derive = "1.0"
serde_yaml = "0.9.28"
serde_json = "1.0"
base64 = "~0.22.1"
futures = "0.3.30"
hyper = "1.3.1"
url = "2.5.0"
oauth2 = "4.4.2"
reqwest = { version = "0.12", features = ["blocking", "json"] }
jsonwebtoken = "9.3.0"
tokio = { version = "1", features = ["full"] }
sqlx = { version = "0.7", features = ["sqlite", "runtime-tokio-native-tls"] }
anyhow = "1.0"
bcrypt = "0.15.1"
itertools = "0.13"
indoc = "1.0.3"
[dev-dependencies]
tokio-core = "*"

View File

@ -1,11 +1,22 @@
# Vote-rs
Currently in development
Currently in development....
## current functionality
Converts a csv file into a `.blt`file [(OpenSTV/OpaVote ballots)](https://www.opavote.com/help/overview#blt-file-format).
stared on mocups and api documentation for a possible api to the application.
## Goal
Create a webapplication to commit, some simple elections.
as there already seem to exist multiple diffrent voting result libraries/application in rust,
the aim of this is rather to create the webui for participants to submit their votes, and use a library to get the results after a election is done.
## Building
```bash
cargo +nightly -Z unstable-options build --out-dir ./build
cargo -Z unstable-options build --out-dir ./build
```

12
docs/Authorization.md Normal file
View File

@ -0,0 +1,12 @@
# Authorization
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**to_date** | **String** | | [optional] [default to null]
**from_date** | **String** | | [optional] [default to null]
**user** | **String** | | [optional] [default to null]
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)

11
docs/Credentials.md Normal file
View File

@ -0,0 +1,11 @@
# Credentials
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**username** | **String** | | [optional] [default to null]
**password** | **String** | | [optional] [default to null]
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)

174
docs/DefaultApi.md Normal file
View File

@ -0,0 +1,174 @@
# \DefaultApi
All URIs are relative to *https://localhost/api*
Method | HTTP request | Description
------------- | ------------- | -------------
[**auth_login_post**](DefaultApi.md#auth_login_post) | **Post** /auth/login | Authenticate user
[**auth_token_post**](DefaultApi.md#auth_token_post) | **Post** /auth/token | Generate authentication token for another user
[**elections_all_get**](DefaultApi.md#elections_all_get) | **Get** /elections/all | Get all existing elections
[**elections_create_post**](DefaultApi.md#elections_create_post) | **Post** /elections/create | Create new election
[**elections_id_get**](DefaultApi.md#elections_id_get) | **Get** /elections/{id} | Get all existing elections
[**elections_id_post**](DefaultApi.md#elections_id_post) | **Post** /elections/{id} | Vote in exsisting election
# **auth_login_post**
> ::models::InlineResponse200 auth_login_post(ctx, credentials)
Authenticate user
### Required Parameters
Name | Type | Description | Notes
------------- | ------------- | ------------- | -------------
**ctx** | **context.Context** | context containing the authentication | nil if no authentication
**credentials** | [**Credentials**](Credentials.md)| |
### Return type
[**::models::InlineResponse200**](inline_response_200.md)
### Authorization
[JWT](../README.md#JWT)
### HTTP request headers
- **Content-Type**: application/json
- **Accept**: application/json
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
# **auth_token_post**
> ::models::InlineResponse2001 auth_token_post(ctx, authorization, token)
Generate authentication token for another user
### Required Parameters
Name | Type | Description | Notes
------------- | ------------- | ------------- | -------------
**ctx** | **context.Context** | context containing the authentication | nil if no authentication
**authorization** | **String**| Your authorization token |
**token** | [**Authorization**](Authorization.md)| |
### Return type
[**::models::InlineResponse2001**](inline_response_200_1.md)
### Authorization
[JWT](../README.md#JWT)
### HTTP request headers
- **Content-Type**: application/json
- **Accept**: application/json
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
# **elections_all_get**
> ::models::ElectionList elections_all_get(ctx, authorization)
Get all existing elections
### Required Parameters
Name | Type | Description | Notes
------------- | ------------- | ------------- | -------------
**ctx** | **context.Context** | context containing the authentication | nil if no authentication
**authorization** | **String**| Your authorization token |
### Return type
[**::models::ElectionList**](ElectionList.md)
### Authorization
[JWT](../README.md#JWT)
### HTTP request headers
- **Content-Type**: application/json
- **Accept**: application/json
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
# **elections_create_post**
> ::models::Election elections_create_post(ctx, authorization, election)
Create new election
### Required Parameters
Name | Type | Description | Notes
------------- | ------------- | ------------- | -------------
**ctx** | **context.Context** | context containing the authentication | nil if no authentication
**authorization** | **String**| Your authorization token |
**election** | [**Election**](Election.md)| |
### Return type
[**::models::Election**](Election.md)
### Authorization
[JWT](../README.md#JWT)
### HTTP request headers
- **Content-Type**: application/json
- **Accept**: application/json
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
# **elections_id_get**
> ::models::Election elections_id_get(ctx, authorization, id)
Get all existing elections
### Required Parameters
Name | Type | Description | Notes
------------- | ------------- | ------------- | -------------
**ctx** | **context.Context** | context containing the authentication | nil if no authentication
**authorization** | **String**| Your authorization token |
**id** | **String**| |
### Return type
[**::models::Election**](Election.md)
### Authorization
[JWT](../README.md#JWT)
### HTTP request headers
- **Content-Type**: application/json
- **Accept**: application/json
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
# **elections_id_post**
> ::models::Vote elections_id_post(ctx, authorization, election)
Vote in exsisting election
### Required Parameters
Name | Type | Description | Notes
------------- | ------------- | ------------- | -------------
**ctx** | **context.Context** | context containing the authentication | nil if no authentication
**authorization** | **String**| Your authorization token |
**election** | [**Vote**](Vote.md)| |
### Return type
[**::models::Vote**](Vote.md)
### Authorization
[JWT](../README.md#JWT)
### HTTP request headers
- **Content-Type**: application/json
- **Accept**: application/json
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)

16
docs/Election.md Normal file
View File

@ -0,0 +1,16 @@
# Election
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**id** | **String** | | [optional] [default to null]
**username** | **String** | | [optional] [default to null]
**name** | **String** | | [optional] [default to null]
**description** | **String** | | [optional] [default to null]
**start_date** | **String** | | [optional] [default to null]
**end_date** | **String** | | [optional] [default to null]
**items** | [**Vec<::models::ElectionItem>**](ElectionItem.md) | | [optional] [default to null]
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)

11
docs/ElectionItem.md Normal file
View File

@ -0,0 +1,11 @@
# ElectionItem
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**id** | **String** | | [optional] [default to null]
**name** | **String** | | [optional] [default to null]
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)

9
docs/ElectionList.md Normal file
View File

@ -0,0 +1,9 @@
# ElectionList
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)

10
docs/InlineResponse200.md Normal file
View File

@ -0,0 +1,10 @@
# InlineResponse200
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**token** | **String** | | [optional] [default to null]
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)

View File

@ -0,0 +1,10 @@
# InlineResponse2001
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**token** | **String** | | [optional] [default to null]
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)

12
docs/User.md Normal file
View File

@ -0,0 +1,12 @@
# User
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**id** | **String** | | [optional] [default to null]
**username** | **String** | | [optional] [default to null]
**password** | **String** | | [optional] [default to null]
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)

12
docs/Vote.md Normal file
View File

@ -0,0 +1,12 @@
# Vote
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**authorization** | [***::models::Authorization**](Authorization.md) | | [optional] [default to null]
**userid** | **String** | | [optional] [default to null]
**data** | [**Vec<::models::VoteItem>**](VoteItem.md) | | [optional] [default to null]
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)

11
docs/VoteItem.md Normal file
View File

@ -0,0 +1,11 @@
# VoteItem
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**item** | [***::models::ElectionItem**](ElectionItem.md) | | [optional] [default to null]
**value** | **f32** | | [optional] [default to null]
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)

66
flake.lock Normal file
View File

@ -0,0 +1,66 @@
{
"nodes": {
"fenix": {
"inputs": {
"nixpkgs": [
"nixpkgs"
],
"rust-analyzer-src": "rust-analyzer-src"
},
"locked": {
"lastModified": 1716618425,
"narHash": "sha256-eZs7f4izo6t0AmOI1IAU6/ZbbXrxMPGdo+khe4hP3Rk=",
"owner": "nix-community",
"repo": "fenix",
"rev": "9a9fafd0c3f796b675acb2e16ae238d4fd2cbdb5",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "fenix",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1716509168,
"narHash": "sha256-4zSIhSRRIoEBwjbPm3YiGtbd8HDWzFxJjw5DYSDy1n8=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "bfb7a882678e518398ce9a31a881538679f6f092",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"fenix": "fenix",
"nixpkgs": "nixpkgs"
}
},
"rust-analyzer-src": {
"flake": false,
"locked": {
"lastModified": 1716572615,
"narHash": "sha256-mVUbarr4PNjERDk+uaoitPq7eL7De0ythZehezAzug8=",
"owner": "rust-lang",
"repo": "rust-analyzer",
"rev": "a55e8bf09cdfc25066b77823cc98976a51af8a8b",
"type": "github"
},
"original": {
"owner": "rust-lang",
"ref": "nightly",
"repo": "rust-analyzer",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

View File

@ -1,28 +1,37 @@
{
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
devenv.url = "github:cachix/devenv";
fenix.url = "github:nix-community/fenix";
fenix.inputs.nixpkgs.follows = "nixpkgs";
};
outputs = { self, nixpkgs, devenv, ... }@inputs:
outputs = { self, nixpkgs, fenix }@inputs:
let
pkgs = import nixpkgs {
system = "x86_64-linux";
};
systems = [
"x86_64-linux"
"aarch64-linux"
"aarch64-darwin"
];
forAllSystems = f: nixpkgs.lib.genAttrs systems (system: let
toolchain = fenix.packages.${system}.complete;
pkgs = import nixpkgs {
inherit system;
overlays = [
(_: super: let pkgs = fenix.inputs.nixpkgs.legacyPackages.${system}; in fenix.overlays.default pkgs pkgs)
];
};
in f system pkgs toolchain);
in {
devShell.x86_64-linux = devenv.lib.mkShell {
inherit inputs pkgs;
modules = [
{
languages.rust = {
enable = true;
channel = "stable";
};
}
devShell = forAllSystems (system: pkgs: toolchain: pkgs.mkShell {
packages = [
(toolchain.withComponents [
"cargo" "rustc" "rustfmt" "clippy"
])
pkgs.openssl
pkgs.pkg-config
];
};
RUST_SRC_PATH = "${toolchain.rust-src}/lib/rustlib/src/rust/library";
});
};
}

303
openapi.yaml Normal file
View File

@ -0,0 +1,303 @@
swagger: "2.0"
info:
version: "0.0.1"
title: vote-rs API
description: API for conducting electronic voting
basePath: /api
schemes:
- https
consumes:
- application/json
produces:
- application/json
securityDefinitions:
JWT:
type: apiKey
name: Authorization
in: header
security:
- JWT: []
definitions:
User:
type: object
properties:
username:
type: string
password:
type: string
Authorization:
type: object
properties:
username:
type: string
namespace:
type: string
AuthorizationItems:
type: array
items:
$ref: "#/definitions/Authorization"
Election:
type: object
properties:
username:
type: string
namespace:
type: string
name:
type: string
description:
type: string
start_date:
type: string
format: date-time
end_date:
type: string
format: date-time
items:
type: array
items:
$ref: "#/definitions/ElectionItem"
ElectionItem:
type: object
properties:
name:
type: string
ElectionList:
type: array
items:
$ref: "#/definitions/Election"
VoteItem:
type: object
properties:
item:
$ref: "#/definitions/ElectionItem"
value:
type: number
Vote:
type: object
properties:
userid:
type: string
data:
type: array
items:
$ref: "#/definitions/VoteItem"
paths:
/auth/login:
post:
summary: Authenticate user
consumes:
- application/json
produces:
- application/json
parameters:
- in: body
name: credentials
required: true
schema:
type: object
properties:
username:
type: string
password:
type: string
format: password
responses:
200:
description: Login successful
schema:
type: object
properties:
token:
type: string
401:
description: Unauthorized
/auth/authorization:
post:
summary: Generate authentication token for another user
consumes:
- application/json
produces:
- application/json
parameters:
- in: header
name: Authorization
description: Your authorization token
required: true
type: string
format: JWT
- in: body
name: authorization
required: true
schema:
$ref: "#/definitions/Authorization"
responses:
200:
description: Token generated successfully
schema:
type: object
properties:
token:
type: string
format: JWT
401:
description: Unauthorized
get:
summary: Generate authentication token for another user
consumes:
- application/json
produces:
- application/json
parameters:
- in: header
name: Authorization
description: Your authorization token
required: true
type: string
format: JWT
responses:
200:
description: List of authorizations you have gotten.
schema:
$ref: "#/definitions/AuthorizationItems"
401:
description: Unauthorized
delete:
summary: Delete all Authorization you have given to a user
consumes:
- application/json
produces:
- application/json
parameters:
- in: header
name: Authorization
description: Your authorization token
required: true
type: string
format: JWT
- in: body
name: Authorisation
description: The authorization token you want to delete
schema:
$ref: "#/definitions/Authorization"
responses:
200:
description: Sucsess
401:
description: Unauthorized
/elections:
post:
summary: Create new election
consumes:
- application/json
produces:
- application/json
parameters:
- in: header
name: Authorization
description: Your authorization token
required: true
type: string
format: JWT
- in: body
name: election
required: true
schema:
$ref: "#/definitions/Election"
responses:
201:
description: Election created successfully
schema:
$ref: "#/definitions/Election"
401:
description: Unauthorized
get:
summary: Get all existing elections
parameters:
- in: header
name: Authorization
description: Your authorization token
required: true
type: string
format: JWT
responses:
200:
description: List of all existing elections
schema:
$ref: "#/definitions/ElectionList"
401:
description: Unauthorized
/elections/{id}:
get:
summary: Get all existing elections
parameters:
- in: header
name: Authorization
description: Your authorization token
required: true
type: string
format: JWT
- in: path
name: id
type: string
required: true
responses:
200:
description: List of all existing elections
schema:
$ref: "#/definitions/Election"
401:
description: Unauthorized
post:
summary: Vote in exsisting election
consumes:
- application/json
produces:
- application/json
parameters:
- in: header
name: Authorization
description: Your authorization token
required: true
type: string
format: JWT
- in: body
name: election
required: true
schema:
$ref: "#/definitions/Vote"
responses:
201:
description: Election created successfully
schema:
$ref: "#/definitions/Vote"
401:
description: Unauthorized

118
src/api.rs Normal file
View File

@ -0,0 +1,118 @@
#![feature(proc_macro_hygiene, decl_macro)]
#[macro_use] extern crate rocket;
#[macro_use] extern crate rocket_contrib;
#[macro_use] extern crate serde_derive;
use rocket::http::Status;
use rocket_contrib::json::{Json, JsonValue};
use crate::auth::login;
// Define data models
#[derive(Serialize, Deserialize)]
struct User {
id: String,
username: String,
password: String,
}
#[derive(Serialize, Deserialize)]
struct Authorization {
to_date: String,
from_date: String,
user: String,
}
#[derive(Serialize, Deserialize)]
struct Election {
id: String,
username: String,
name: String,
description: String,
start_date: String,
end_date: String,
items: Vec<ElectionItem>,
}
#[derive(Serialize, Deserialize)]
struct ElectionItem {
id: String,
name: String,
}
#[derive(Serialize, Deserialize)]
struct VoteItem {
item: ElectionItem,
value: f64,
}
#[derive(Serialize, Deserialize)]
struct Vote {
authorization: Authorization,
userid: String,
data: Vec<VoteItem>,
}
#[post("/auth/login", format = "application/json", data = "<credentials>")]
async fn handle_login(credentials: Json<User>, db: Db) -> JsonValue {
match login(credentials.email, credentials.password, db).await {
Ok(token) => json!({
"token": token
}),
Err(error) => json!({
"error": error
}),
}
}
#[post("/auth/token", format = "application/json", data = "<token>")]
fn generate_token(token: Json<Authorization>) -> JsonValue {
// Token generation logic here
json!({
"token": "generated_token"
})
}
#[post("/elections/create", format = "application/json", data = "<election>")]
fn create_election(election: Json<Election>) -> Result<JsonValue, Status> {
// Election creation logic here
Ok(json!(election))
}
#[get("/elections/all")]
fn get_all_elections() -> JsonValue {
// Retrieve all elections logic here
json!([
// List of all existing elections
])
}
#[get("/elections/<id>")]
fn get_election(id: String) -> JsonValue {
// Retrieve single election logic here
json!({
"id": id,
// Other election details
})
}
#[post("/elections/<id>", format = "application/json", data = "<vote>")]
fn vote_in_election(id: String, vote: Json<Vote>) -> Result<JsonValue, Status> {
// Voting logic here
Ok(json!(vote))
}
// Rocket fairings to set up CORS and other middlewares can be added here
fn main() {
rocket::ignite()
.mount("/api", routes![
login,
generate_token,
create_election,
get_all_elections,
get_election,
vote_in_election,
])
.launch();
}

33
src/auth.rs Normal file
View File

@ -0,0 +1,33 @@
use jsonwebtoken::{encode, Header, EncodingKey};
use serde::{Serialize, Deserialize};
use chrono::{Utc, Duration};
use std::error::Error;
#[derive(Debug, Serialize, Deserialize)]
struct Claims {
sub: String,
exp: usize,
}
pub async fn login(username: &str, _password: &str, _db: &sqlx::SqlitePool) -> Result<String, Box<dyn Error>> {
// Normally, you would validate the username and password against the database here.
// For the mock, we just generate a token for any username.
let expiration = Utc::now()
.checked_add_signed(Duration::minutes(60))
.expect("valid timestamp")
.timestamp() as usize;
let claims = Claims {
sub: username.to_owned(),
exp: expiration,
};
let token = encode(
&Header::default(),
&claims,
&EncodingKey::from_secret("secret".as_ref()),
)?;
Ok(token)
}

239
src/db.rs Normal file
View File

@ -0,0 +1,239 @@
use sqlx::{sqlite::SqlitePool, Pool, Row};
use std::error::Error;
use serde::{Deserialize, Serialize};
use indoc::indoc;
pub struct Connection {
pool: Pool<sqlx::Sqlite>,
}
pub async fn connect() -> Result<Connection, Box<dyn Error>> {
let database_url = "sqlite::memory:"; // Use an in-memory SQLite database for testing
let pool = SqlitePool::connect(database_url).await?;
Ok(Connection { pool })
}
pub async fn init_db(conn: &Connection) -> Result<(), Box<dyn Error>> {
sqlx::query(indoc! {"
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT NOT NULL UNIQUE,
jwt_token TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS elections (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
username TEXT NOT NULL,
namespace TEXT NOT NULL,
description TEXT,
start_date TEXT NOT NULL,
end_date TEXT NOT NULL,
FOREIGN KEY (username) REFERENCES users(username)
);
CREATE TABLE IF NOT EXISTS authorizations (
id INTEGER PRIMARY KEY AUTOINCREMENT,
from_date TEXT NOT NULL,
to_date TEXT NOT NULL,
from_user TEXT NOT NULL,
to_user TEXT NOT NULL,
namespace TEXT NOT NULL,
FOREIGN KEY (from_user) REFERENCES users(username),
FOREIGN KEY (to_user) REFERENCES users(username)
);
CREATE TABLE IF NOT EXISTS votes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
value INTEGER NOT NULL,
option_id INTEGER NOT NULL,
election_id INTEGER NOT NULL,
option_name TEXT NOT NULL,
user TEXT NOT NULL,
namespace TEXT NOT NULL,
date TEXT NOT NULL,
FOREIGN KEY (election_id) REFERENCES elections(id),
FOREIGN KEY (user) REFERENCES users(username)
);
"})
.execute(&conn.pool)
.await?;
Ok(())
}
// Users
#[derive(Serialize, Deserialize)]
pub struct User {
pub id: i64,
pub username: String,
pub jwt_token: String,
}
pub async fn insert_user(conn: &Connection, username: &str, jwt_token: &str) -> Result<(), Box<dyn Error>> {
sqlx::query("INSERT INTO users (username, jwt_token) VALUES (?, ?)")
.bind(username)
.bind(jwt_token)
.execute(&conn.pool)
.await?;
Ok(())
}
pub async fn get_user(conn: &Connection, username: &str) -> Result<Option<User>, Box<dyn Error>> {
let row = sqlx::query("SELECT id, username, jwt_token FROM users WHERE username = ?")
.bind(username)
.fetch_optional(&conn.pool)
.await?;
if let Some(row) = row {
Ok(Some(User {
id: row.get("id"),
username: row.get("username"),
jwt_token: row.get("jwt_token"),
}))
} else {
Ok(None)
}
}
// Elections
#[derive(Serialize, Deserialize)]
pub struct Election {
pub id: i64,
pub name: String,
pub username: String,
pub namespace: String,
pub description: String,
pub start_date: String,
pub end_date: String,
}
pub async fn insert_election(conn: &Connection, election: &Election) -> Result<(), Box<dyn Error>> {
sqlx::query(indoc! {"
INSERT INTO elections (name, username, namespace, description, start_date, end_date)
VALUES (?, ?, ?, ?, ?, ?)
"})
.bind(&election.name)
.bind(&election.username)
.bind(&election.namespace)
.bind(&election.description)
.bind(&election.start_date)
.bind(&election.end_date)
.execute(&conn.pool)
.await?;
Ok(())
}
pub async fn get_election(conn: &Connection, id: i64) -> Result<Option<Election>, Box<dyn Error>> {
let row = sqlx::query("SELECT id, name, username, namespace, description, start_date, end_date FROM elections WHERE id = ?")
.bind(id)
.fetch_optional(&conn.pool)
.await?;
if let Some(row) = row {
Ok(Some(Election {
id: row.get("id"),
name: row.get("name"),
username: row.get("username"),
namespace: row.get("namespace"),
description: row.get("description"),
start_date: row.get("start_date"),
end_date: row.get("end_date"),
}))
} else {
Ok(None)
}
}
// Authorizations
#[derive(Serialize, Deserialize)]
pub struct Authorization {
pub id: i64,
pub from_date: String,
pub to_date: String,
pub from_user: String,
pub to_user: String,
pub namespace: String,
}
pub async fn insert_authorization(conn: &Connection, authorization: &Authorization) -> Result<(), Box<dyn Error>> {
sqlx::query(indoc! {"
INSERT INTO authorizations (from_date, to_date, from_user, to_user, namespace)
VALUES (?, ?, ?, ?, ?)
"})
.bind(&authorization.from_date)
.bind(&authorization.to_date)
.bind(&authorization.from_user)
.bind(&authorization.to_user)
.bind(&authorization.namespace)
.execute(&conn.pool)
.await?;
Ok(())
}
pub async fn get_authorization(conn: &Connection, id: i64) -> Result<Option<Authorization>, Box<dyn Error>> {
let row = sqlx::query("SELECT id, from_date, to_date, from_user, to_user, namespace FROM authorizations WHERE id = ?")
.bind(id)
.fetch_optional(&conn.pool)
.await?;
if let Some(row) = row {
Ok(Some(Authorization {
id: row.get("id"),
from_date: row.get("from_date"),
to_date: row.get("to_date"),
from_user: row.get("from_user"),
to_user: row.get("to_user"),
namespace: row.get("namespace"),
}))
} else {
Ok(None)
}
}
// Votes
#[derive(Serialize, Deserialize)]
pub struct Vote {
pub id: i64,
pub value: i32,
pub option_id: i64,
pub election_id: i64,
pub option_name: String,
pub user: String,
pub namespace: String,
pub date: String,
}
pub async fn insert_vote(conn: &Connection, vote: &Vote) -> Result<(), Box<dyn Error>> {
sqlx::query(indoc! {"
INSERT INTO votes (value, option_id, election_id, option_name, user, namespace, date)
VALUES (?, ?, ?, ?, ?, ?, ?)
"})
.bind(vote.value)
.bind(vote.option_id)
.bind(vote.election_id)
.bind(&vote.option_name)
.bind(&vote.user)
.bind(&vote.namespace)
.bind(&vote.date)
.execute(&conn.pool)
.await?;
Ok(())
}
pub async fn get_vote(conn: &Connection, id: i64) -> Result<Option<Vote>, Box<dyn Error>> {
let row = sqlx::query("SELECT id, value, option_id, election_id, option_name, user, namespace, date FROM votes WHERE id = ?")
.bind(id)
.fetch_optional(&conn.pool)
.await?;
if let Some(row) = row {
Ok(Some(Vote {
id: row.get("id"),
value: row.get("value"),
option_id: row.get("option_id"),
election_id: row.get("election_id"),
option_name: row.get("option_name"),
user: row.get("user"),
namespace: row.get("namespace"),
date: row.get("date"),
}))
} else {
Ok(None)
}
}

0
src/lib.rs Normal file
View File

View File

@ -1,61 +1,186 @@
#[macro_use]
extern crate rocket;
// main.rs
use rocket::form::Form;
use rocket::response::Redirect;
use rocket::State; // Add this line
use rocket::fs::FileServer;
use rocket::serde::json::Json;
use rocket_dyn_templates::Template;
use std::sync::Mutex;
use std::collections::HashMap;
use csv::Writer;
mod db;
mod voting;
#[cfg(test)]
mod tests {
use super::db;
use tokio;
#[derive(rocket::FromForm)]
struct Vote {
option: String,
#[tokio::test]
async fn test_connection() {
let result = db::connect().await;
assert!(result.is_ok(), "Database connection failed");
}
#[tokio::test]
async fn test_insert_user() {
let conn = db::connect().await.unwrap();
db::init_db(&conn).await.unwrap();
let result = db::insert_user(&conn, "test_user", "test_jwt").await;
assert!(result.is_ok(), "Insert user operation failed");
}
#[tokio::test]
async fn test_get_user() {
let conn = db::connect().await.unwrap();
db::init_db(&conn).await.unwrap();
db::insert_user(&conn, "test_user", "test_jwt").await.unwrap();
let result = db::get_user(&conn, "test_user").await;
assert!(result.is_ok(), "Get user operation failed");
let user = result.unwrap();
assert!(user.is_some(), "User not found");
let user = user.unwrap();
assert_eq!(user.username, "test_user", "Username does not match");
assert_eq!(user.jwt_token, "test_jwt", "JWT token does not match");
}
#[tokio::test]
async fn test_insert_election() {
let conn = db::connect().await.unwrap();
db::init_db(&conn).await.unwrap();
db::insert_user(&conn, "test_user", "test_jwt").await.unwrap();
let election = db::Election {
id: 0,
name: "Election 1".to_string(),
username: "test_user".to_string(),
namespace: "namespace2".to_string(),
description: "Description 1".to_string(),
start_date: "2024-05-01T08:00".to_string(),
end_date: "2024-05-10T20:00".to_string(),
};
let result = db::insert_election(&conn, &election).await;
assert!(result.is_ok(), "Insert election operation failed");
}
#[tokio::test]
async fn test_get_election() {
let conn = db::connect().await.unwrap();
db::init_db(&conn).await.unwrap();
db::insert_user(&conn, "test_user", "test_jwt").await.unwrap();
let election = db::Election {
id: 0,
name: "Election 1".to_string(),
username: "test_user".to_string(),
namespace: "namespace2".to_string(),
description: "Description 1".to_string(),
start_date: "2024-05-01T08:00".to_string(),
end_date: "2024-05-10T20:00".to_string(),
};
db::insert_election(&conn, &election).await.unwrap();
let result = db::get_election(&conn, 1).await;
assert!(result.is_ok(), "Get election operation failed");
let election = result.unwrap();
assert!(election.is_some(), "Election not found");
let election = election.unwrap();
assert_eq!(election.name, "Election 1", "Election name does not match");
}
#[tokio::test]
async fn test_insert_authorization() {
let conn = db::connect().await.unwrap();
db::init_db(&conn).await.unwrap();
db::insert_user(&conn, "user1", "jwt1").await.unwrap();
db::insert_user(&conn, "user2", "jwt2").await.unwrap();
let authorization = db::Authorization {
id: 0,
from_date: "2024-05-01T00:00".to_string(),
to_date: "2024-05-10T23:59".to_string(),
from_user: "user1".to_string(),
to_user: "user2".to_string(),
namespace: "namespace".to_string(),
};
let result = db::insert_authorization(&conn, &authorization).await;
assert!(result.is_ok(), "Insert authorization operation failed");
}
#[tokio::test]
async fn test_get_authorization() {
let conn = db::connect().await.unwrap();
db::init_db(&conn).await.unwrap();
db::insert_user(&conn, "user1", "jwt1").await.unwrap();
db::insert_user(&conn, "user2", "jwt2").await.unwrap();
let authorization = db::Authorization {
id: 0,
from_date: "2024-05-01T00:00".to_string(),
to_date: "2024-05-10T23:59".to_string(),
from_user: "user1".to_string(),
to_user: "user2".to_string(),
namespace: "namespace".to_string(),
};
db::insert_authorization(&conn, &authorization).await.unwrap();
let result = db::get_authorization(&conn, 1).await;
assert!(result.is_ok(), "Get authorization operation failed");
let authorization = result.unwrap();
assert!(authorization.is_some(), "Authorization not found");
let authorization = authorization.unwrap();
assert_eq!(authorization.from_user, "user1", "From user does not match");
assert_eq!(authorization.to_user, "user2", "To user does not match");
}
#[tokio::test]
async fn test_insert_vote() {
let conn = db::connect().await.unwrap();
db::init_db(&conn).await.unwrap();
db::insert_user(&conn, "user1", "jwt1").await.unwrap();
db::insert_election(&conn, &db::Election {
id: 0,
name: "Election 1".to_string(),
username: "user1".to_string(),
namespace: "namespace".to_string(),
description: "Description 1".to_string(),
start_date: "2024-05-01T08:00".to_string(),
end_date: "2024-05-10T20:00".to_string(),
}).await.unwrap();
let vote = db::Vote {
id: 0,
value: 1,
option_id: 1,
election_id: 1,
option_name: "Option 1".to_string(),
user: "user1".to_string(),
namespace: "namespace".to_string(),
date: "2024-05-01T12:00".to_string(),
};
let result = db::insert_vote(&conn, &vote).await;
assert!(result.is_ok(), "Insert vote operation failed");
}
#[tokio::test]
async fn test_get_vote() {
let conn = db::connect().await.unwrap();
db::init_db(&conn).await.unwrap();
db::insert_user(&conn, "user1", "jwt1").await.unwrap();
db::insert_election(&conn, &db::Election {
id: 0,
name: "Election 1".to_string(),
username: "user1".to_string(),
namespace: "namespace".to_string(),
description: "Description 1".to_string(),
start_date: "2024-05-01T08:00".to_string(),
end_date: "2024-05-10T20:00".to_string(),
}).await.unwrap();
let vote = db::Vote {
id: 0,
value: 1,
option_id: 1,
election_id: 1,
option_name: "Option 1".to_string(),
user: "user1".to_string(),
namespace: "namespace".to_string(),
date: "2024-05-01T12:00".to_string(),
};
db::insert_vote(&conn, &vote).await.unwrap();
let result = db::get_vote(&conn, 1).await;
assert!(result.is_ok(), "Get vote operation failed");
let vote = result.unwrap();
assert!(vote.is_some(), "Vote not found");
let vote = vote.unwrap();
assert_eq!(vote.option_name, "Option 1", "Option name does not match");
}
}
type Votes = Mutex<Writer<std::fs::File>>;
#[post("/vote", data = "<vote>")]
fn vote(vote: Form<Vote>, votes: &State<Votes>) -> Redirect {
let mut writer = votes.lock().unwrap();
writer.write_record(&[vote.option.clone()]).unwrap();
writer.flush().unwrap();
Redirect::to("/")
#[tokio::main]
async fn main() {
println!("This is the main function. Run `cargo test` to execute tests.");
}
#[post("/results")]
fn results() -> Json<HashMap<String, String>> {
let election_data = voting::csv_to_election_data("votes.csv", 1, "Election".to_string());
let blt = voting::election_data_to_blt(election_data);
let results = voting::run_blt(blt);
Json(results)
}
#[get("/")]
fn index() -> Template {
let context: HashMap<&str, &str> = HashMap::new();
Template::render("index", &context)
}
#[launch]
fn rocket() -> _ {
let file = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open("votes.csv")
.unwrap();
let writer = csv::Writer::from_writer(file);
rocket::build()
.attach(Template::fairing())
.manage(Mutex::new(writer))
.mount("/", routes![index, vote])
.mount("/static", FileServer::from("static"))
}

View File

@ -0,0 +1,74 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Create Election</title>
<style>
body {
margin: 0;
padding: 0;
background-color: #1a1a1a;
color: #9f9f9f;
}
.container {
width: 25em;
margin: 3.125em auto;
padding: 1.25em;
background-color: #2b2b2b;
border-radius: 0.3125em;
box-shadow: 0 0.125em 0.3125em rgba(0, 0, 0, 0.1);
}
h2 {
text-align: center;
margin-bottom: 1.25em;
}
input[type="text"],
input[type="datetime-local"],
textarea,
input[type="submit"] {
width: 100%;
padding: 0.625em;
margin-bottom: 0.625em;
border: 0.0125em solid #868686;
border-radius: 0.3125em;
box-sizing: border-box;
background-color: #2b2b2b;
color: #9f9f9f;
}
textarea {
height: 6.25em;
background-color: #2b2b2b;
color: #9f9f9f;
}
input[type="submit"] {
background-color: #1a75ff; /* Darker blue */
color: white;
cursor: pointer;
border: 0px;
}
input[type="submit"]:hover {
background-color: #145cbf; /* Even darker blue */
}
</style>
</head>
<body>
<div class="container">
<h2>Create Election</h2>
<form id="electionForm" action="/elections/create" method="POST">
<label for="name">Name:</label>
<input type="text" id="name" name="name" placeholder="Enter election name" required>
<label for="description">Description:</label>
<textarea id="description" name="description" placeholder="Enter election description" required></textarea>
<label for="start_date">Start Date:</label>
<input type="datetime-local" id="start_date" name="start_date" required>
<label for="end_date">End Date:</label>
<input type="datetime-local" id="end_date" name="end_date" required>
<label for="namespace">Namespace:</label>
<input type="text" id="namespace" name="namespace" placeholder="Enter namespace" required>
<!-- Additional fields for election items can be added here -->
<input type="submit" value="Create Election">
</form>
</div>
</body>
</html>

View File

@ -0,0 +1,138 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Election Details</title>
<style>
body {
margin: 0;
padding: 0;
background-color: #1a1a1a;
color: #9f9f9f;
}
h2 {
grid-column: span 2;
text-align: center;
margin-bottom: 1.25em;
}
button {
padding: 0.3125em 0.625em;
border: none;
border-radius: 0.3125em;
color: white;
cursor: pointer;
width: 6em;
}
.detail {
width: 48%; /* Adjust as needed */
padding: 0.5em;
background-color: #333; /* Dark gray */
border-radius: 0.3125em;
margin-bottom: 1em;
box-sizing: border-box; /* Add this line */
}
.detail.full-width {
width: 100%; /* Full width */
}
.detail.full-heigth {
height: 100%; /* Full height */
min-height: 8.25em; /* Minimum height */
}
.detail.dates {
display: flex; /* Add this line */
justify-content: space-between; /* Add this line */
}
.detail label {
font-weight: bold;
margin-right: 0.5em;
}
.detail span {
color: #9f9f9f;
}
.go-btn {
background-color: #4CAF50; /* Green */
}
.delete-btn {
background-color: #f44336; /* Red */
}
.results-btn {
background-color: #008CBA; /* Blue */
}
.button-container {
display: flex;
justify-content: space-evenly;
width: 100%;
margin-top: 1em;
}
.container {
display: flex;
flex-direction: column;
align-items: center;
width: 50em;
margin: 5em auto;
padding: 3em;
background-color: #2b2b2b;
border-radius: 0.3125em;
box-shadow: 0 0.125em 0.3125em rgba(0, 0, 0, 0.1);
}
#electionDetails {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
width: 100%;
}
</style>
</head>
<body>
<div class="container">
<h2>Election Details</h2>
<div id="electionDetails">
<!-- Election details will be inserted here dynamically -->
</div>
<div class="button-container">
<button class="go-btn">Vote</button>
<button class="delete-btn" onclick="confirmDelete()">Delete</button>
<button class="results-btn" onclick="confirmResults()">Results</button>
</div>
</div>
<script>
function confirmDelete() {
if (confirm("Are you sure you want to delete this election?")) {
// Implement the logic to delete the election
}
}
function confirmResults() {
if (confirm("Are you sure you want to view the results of this election?")) {
// Implement the logic to view the results
}
}
// Dummy data for demonstration
const election = { id: 1, name: "Election 1", username: "user", namespace: "namespace2", description: "Description 1", start_date: "2024-05-01T08:00", end_date: "2024-05-10T20:00" };
// Function to render election details
function renderElectionDetails(election) {
const detailsDiv = document.querySelector("#electionDetails");
detailsDiv.innerHTML = `
<div class="detail"><label>ID:</label> <span>${election.id}</span></div>
<div class="detail"><label>User:</label> <span>${election.username}</span></div>
<div class="detail"><label>Name:</label> <span>${election.name}</span></div>
<div class="detail"><label>Namespace:</label> <span>${election.namespace}</span></div>
<div class="detail full-width full-heigth"><label>Description:</label> <span>${election.description}</span></div>
<div class="detail">Start Date:</label> <span>${new Date(election.start_date).toLocaleString()}</span></div>
<div class="detail">End Date:</label> <span>${new Date(election.end_date).toLocaleString()}</span></div>
`;
}
// Initial rendering of election details
renderElectionDetails(election);
</script>
</body>
</html>

View File

@ -0,0 +1,114 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>View Elections</title>
<style>
body {
margin: 0;
padding: 0;
background-color: #1a1a1a;
color: #9f9f9f;
}
.container {
width: 60em;
margin: 5em auto;
padding: 3em;
background-color: #2b2b2b;
border-radius: 0.3125em;
box-shadow: 0 0.125em 0.3125em rgba(0, 0, 0, 0.1);
}
h2 {
text-align: center;
margin-bottom: 1.25em;
}
#searchInput {
width: 100%;
padding: 0.5em;
margin-bottom: 4em;
background-color: #363535;
color: #9f9f9f;
border: 0.0625em solid #9f9f9f;
}
.card {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1em;
background-color: #333;
border-radius: 0.3125em;
margin-bottom: 1em;
}
button {
padding: 0.3125em 0.625em;
margin: 0 0.3125em;
border: none;
border-radius: 0.3125em;
color: white;
cursor: pointer;
}
.details-btn {
background-color: #4CAF50; /* Green */
}
.delete-btn {
background-color: #f44336; /* Red */
}
</style>
</head>
<body>
<div class="container">
<h2>View Elections</h2>
<input id="searchInput" type="text" placeholder="Search elections..." oninput="searchElections(this.value)">
<div id="electionsContainer">
<!-- Election cards will be inserted here dynamically -->
</div>
</div>
<script>
const elections = [
{ name: "Election 1", username: "user", namespace: "namespace2" },
{ name: "Election 2", username: "user", namespace: "namespace" },
{ name: "Election 3", username: "user2", namespace: "namespace" }
];
const currentUsername = "user";
function renderElections(elections) {
const container = document.querySelector("#electionsContainer");
container.innerHTML = "";
elections.forEach(election => {
const card = document.createElement("div");
card.className = "card";
card.innerHTML = `
<div>
<h3>${election.name}</h3>
<p>Created by ${election.username} in ${election.namespace}</p>
</div>
<div>
<button class="details-btn" onclick="viewDetails('${election.name}')">Details</button>
${election.username === currentUsername ?
`<button class="delete-btn" onclick="if(confirm('Are you sure you want to delete this election?')) { deleteElection('${election.name}') }">Delete</button>`
: ''}
</div>
`;
container.appendChild(card);
});
}
function searchElections(query) {
const filtered = elections.filter(election => election.name.toLowerCase().includes(query.toLowerCase()));
renderElections(filtered);
}
function viewDetails(name) {
// Implement the logic to view the details of the election
}
function deleteElection(name) {
// Implement the logic to delete the election
}
renderElections(elections);
</script>
</body>
</html>

View File

@ -0,0 +1,66 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Give Acces</title>
<style>
body {
margin: 0;
padding: 0;
background-color: #1a1a1a;
color: #9f9f9f;
}
.container {
width: 20em;
margin: 6.25em auto;
padding: 2em;
background-color: #2b2b2b;
border-radius: 0.3125em;
box-shadow: 0 0.125em 0.3125em rgba(0, 0, 0, 0.1);
}
h2 {
text-align: center;
margin-bottom: 2em;
}
input[type="text"],
input[type="datetime-local"],
input[type="submit"] {
width: 100%;
padding: 1em;
margin-bottom: 0.625em;
border: 0.0625em solid #9f9f9f;
border-radius: 0.3125em;
box-sizing: border-box;
background-color: #2b2b2b;
color: #9f9f9f;
}
input[type="submit"] {
background-color: #1a75ff; /* Darker blue */
color: white;
cursor: pointer;
border: 0px;
}
input[type="submit"]:hover {
background-color: #145cbf; /* Even darker blue */
}
</style>
</head>
<body>
<div class="container">
<h2>Give acces</h2>
<form id="tokenForm" action="/auth/token" method="POST">
<label for="from_date">From Date:</label>
<input type="datetime-local" id="from_date" name="from_date" required>
<label for="to_date">To Date:</label>
<input type="datetime-local" id="to_date" name="to_date" required>
<label for="user">User:</label>
<input type="text" id="user" name="user" placeholder="Enter username" required>
<label for="namespace">Namespace:</label>
<input type="text" id="namespace" name="namespace" placeholder="Enter namespace" required>
<input type="submit" value="Give Acces">
</form>
</div>
</body>
</html>

59
src/routes/login.html Normal file
View File

@ -0,0 +1,59 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Login</title>
<style>
body {
margin: 0;
padding: 0;
background-color: #1a1a1a;
color: #9f9f9f;
}
.container {
width: 20em;
margin: 6.25em auto;
padding: 2em;
background-color: #2b2b2b;
border-radius: 0.3125em;
box-shadow: 0 0.125em 0.3125em rgba(0, 0, 0, 0.1);
}
h2 {
text-align: center;
margin-bottom: 1.25em;
}
input[type="text"],
input[type="password"],
input[type="submit"] {
width: 100%;
padding: 0.625em;
margin-bottom: 0.625em;
border: 0.0625em solid #9f9f9f;
border-radius: 0.3125em;
box-sizing: border-box;
background-color: #2b2b2b;
color: #9f9f9f;
}
input[type="submit"] {
background-color: #1a75ff; /* Darker blue */
color: white;
cursor: pointer;
border: 0px;
}
input[type="submit"]:hover {
background-color: #145cbf; /* Even darker blue */
}
</style>
</head>
<body>
<div class="container">
<h2>Login</h2>
<form id="loginForm" action="/auth/login" method="POST">
<input type="text" name="username" placeholder="Username" required>
<input type="password" name="password" placeholder="Password" required>
<input type="submit" value="Login">
</form>
</div>
</body>
</html>

157
src/routes/vote.html Normal file
View File

@ -0,0 +1,157 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Voting Page</title>
<style>
body {
margin: 0;
padding: 0;
background-color: #1a1a1a;
color: #9f9f9f;
}
.container {
width: 50em;
margin: 5em auto;
padding: 2em;
background-color: #2b2b2b;
border-radius: 0.3125em;
box-shadow: 0 0.125em 0.3125em rgba(0, 0, 0, 0.1);
}
.election-card {
margin-bottom: 1.25em;
padding: 0.625em;
background-color: #3b3b3b;
border-radius: 0.3125em;
box-shadow: 0 0.125em 0.3125em rgba(0, 0, 0, 0.1);
}
h2 {
text-align: center;
margin-bottom: 1.25em;
}
.item {
margin-bottom: 0.625em;
display: flex;
align-items: center;
}
.item input[type="checkbox"] {
margin-right: 0.625em;
background-color: #2b2b2b;
color: #9f9f9f;
}
button {
margin-left: 0.625em;
padding: 0.625em;
border: 0px;
border-radius: 0.3125em;
box-sizing: border-box;
background-color: #1a75ff; /* Darker blue */
color: white;
cursor: pointer;
}
button:hover {
background-color: #145cbf; /* Even darker blue */
}
</style>
</head>
<body>
<div class="container">
<h2>Voting Page</h2>
<!-- User cards will be inserted here dynamically -->
<div id="usersContainer"></div>
</div>
<script>
const items = [
{ id: 1, name: "Item 1" },
{ id: 2, name: "Item 2" },
{ id: 3, name: "Item 3" }
// Add more items as needed
];
const users = [
{ id: 1, name: "User 1" },
{ id: 2, name: "User 2" },
{ id: 3, name: "User 3" }
// Add more users as needed
];
const usersContainer = document.getElementById("usersContainer");
users.forEach(user => {
const userCard = document.createElement("div");
userCard.className = "election-card";
userCard.innerHTML = `
<h3>${user.name}</h3>
<div class="selectedItemsContainer"></div>
<h3>Unselected Items</h3>
<div class="unselectedItemsContainer">
${items.map(item => `
<div class="item" id="item${user.id}-${item.id}">
<input type="checkbox" id="checkbox${user.id}-${item.id}" onchange="toggleSelection('${user.id}-${item.id}')">
<label for="checkbox${user.id}-${item.id}">${item.name}</label>
</div>
`).join("")}
</div>
<button onclick="submitVote(${user.id})">Submit Vote</button>
`;
usersContainer.appendChild(userCard);
});
function moveUp(itemId) {
const item = document.getElementById(`item${itemId}`);
const previousItem = item.previousElementSibling;
if (previousItem) {
item.parentNode.insertBefore(item, previousItem);
}
}
function moveDown(itemId) {
const item = document.getElementById(`item${itemId}`);
const nextItem = item.nextElementSibling;
if (nextItem) {
item.parentNode.insertBefore(nextItem, item);
}
}
function toggleSelection(itemId) {
const item = document.getElementById(`item${itemId}`);
const checkbox = item.querySelector(`input[type="checkbox"]`);
const userCard = item.parentNode.parentNode;
const selectedItemsContainer = userCard.querySelector('.selectedItemsContainer');
const unselectedItemsContainer = userCard.querySelector('.unselectedItemsContainer');
if (checkbox.checked) {
const upButton = document.createElement('button');
upButton.onclick = function() { moveUp(itemId); };
upButton.textContent = 'Up';
const downButton = document.createElement('button');
downButton.onclick = function() { moveDown(itemId); };
downButton.textContent = 'Down';
item.appendChild(upButton);
item.appendChild(downButton);
selectedItemsContainer.appendChild(item);
} else {
const buttons = item.querySelectorAll('button');
buttons.forEach(button => button.remove());
unselectedItemsContainer.appendChild(item);
}
}
function submitVote(userId) {
const userCard = document.querySelector(`#user${userId}`);
const selectedItemsContainer = userCard.querySelector('.selectedItemsContainer');
const selectedItems = Array.from(selectedItemsContainer.children)
.map(item => item.id.split('-')[1]); // Get the item id
// Implement the logic to submit the vote with the selected items
}
</script>
</body>
</html>

View File

@ -1,37 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>Voting List</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.css">
<style>
#sortable { list-style-type: none; margin: 0; padding: 0; width: 60%; }
#sortable li { margin: 5px; padding: 5px; font-size: 1.2em; height: 2em; }
</style>
</head>
<body>
<h2>Vote List</h2>
<ul id="sortable">
<li class="ui-state-default"><input type="checkbox"> Item 1</li>
<li class="ui-state-default"><input type="checkbox"> Item 2</li>
</ul>
<button id="add">Add Item</button>
<script src="https://code.jquery.com/jquery-1.12.4.js"></script>
<script src="https://code.jquery.com/ui/1.12.1/jquery-ui.js"></script>
<script>
$( function() {
$( "#sortable" ).sortable();
$( "#sortable" ).disableSelection();
let count = 3;
$('#add').click(function() {
if ($('#sortable li').length < 10) {
$('#sortable').append('<li class="ui-state-default"><input type="checkbox"> Item ' + count++ + '</li>');
} else {
alert('Threshold reached');
}
});
} );
</script>
</body>
</html>

View File

@ -8,7 +8,6 @@ use std::path::Path;
use stv_rs::parse::parse_election;
use stv_rs::meek::stv_droop;
use stv_rs::types::Election;
@ -29,9 +28,6 @@ pub fn blt_to_string_results(blt: String) -> String {
#[derive(Clone)]
struct Ballot {
weight: i32,
@ -85,7 +81,10 @@ pub fn election_data_to_blt(election_data: ElectionData) -> String {
return blt;
}
pub fn csv_to_election_data(csv: &str, num_seats: i32, title: String) -> ElectionData {
//TODO: use a proper csv parser, instead of splitting by delimiter
//read the csv file and convert it to a ballot
let delimiter = ",";
@ -139,7 +138,6 @@ pub fn csv_to_election_data(csv: &str, num_seats: i32, title: String) -> Electio
}
ballots.push(ballot);
}
//deduplicate the ballots
let mut deduped_ballots: Vec<Ballot> = Vec::new();
for ballot in ballots {
@ -155,7 +153,6 @@ pub fn csv_to_election_data(csv: &str, num_seats: i32, title: String) -> Electio
}
}
ballots = deduped_ballots;
let election_data = ElectionData {
num_candidates: num_candidates,
num_seats: num_seats,