Compare commits

..

22 Commits

Author SHA1 Message Date
cbaf52256d Remove unused import 2024-10-05 15:52:59 +02:00
8a7c321044 Fixed bug related to new Vote struct, id and submit date can be None
(useful for posting new votes, ie without id and submit date)
2024-10-05 15:09:51 +02:00
98dbcd11f1 Added submit date to votes 2024-10-05 15:08:41 +02:00
c07a304562 Votes div remains to have date displayed per vote 2024-10-04 22:49:51 +02:00
92c067c9ed small fixes (if player not exsits show ?) 2024-10-04 22:49:00 +02:00
164184e5e9 user can add player while voting 2024-10-04 21:00:53 +02:00
9c8e8beaa6 user can add player while voting 2024-10-04 21:00:43 +02:00
28b2411ceb user can add player 2024-10-04 17:28:20 +02:00
fb28221875 added check for token generation (look if exists) (better) 2024-10-04 17:17:46 +02:00
cd1632b533 added check for token generation (look if exists) 2024-10-04 17:15:59 +02:00
01c8248313 bug fix: ids can be wrong if players added/deleted 2024-10-04 17:05:39 +02:00
5a13cf9214 administration done (minimalistic) 2024-10-04 17:05:10 +02:00
894e102322 Custom Response Body, is authorised returning bool, ... 2024-10-02 19:36:50 +02:00
72fd45eb64 admin page (in progress) + better body (in progress) 2024-10-01 21:27:23 +02:00
4cb1ef1df9 admin page (in progress) 2024-09-28 23:28:14 +02:00
c66e854e03 main code + settings update 2024-09-28 23:27:55 +02:00
3924c87b93 register page 2024-09-28 23:27:44 +02:00
a8a913189e unauthorised page 2024-09-28 23:27:33 +02:00
de0ef9dd69 logout page 2024-09-28 23:27:21 +02:00
13265e1ef3 small login css fix 2024-09-28 23:27:10 +02:00
febd1d33f1 login link 2024-09-28 23:26:15 +02:00
4101e824a2 permissions mustn't be null 2024-09-28 23:25:47 +02:00
21 changed files with 1040 additions and 194 deletions

View File

@@ -3,7 +3,7 @@ CREATE TABLE IF NOT EXISTS users
id INTEGER PRIMARY KEY NOT NULL, id INTEGER PRIMARY KEY NOT NULL,
username TEXT NOT NULL, username TEXT NOT NULL,
saltyhash text NOT NULL, saltyhash text NOT NULL,
permissions INTEGER DEFAULT 0, permissions INTEGER DEFAULT 0 NOT NULL,
token TEXT NOT NULL, token TEXT NOT NULL,
UNIQUE(id, username) UNIQUE(id, username)
) )

View File

@@ -1,8 +1,11 @@
{ {
"/": "static/html/index.html", "/": {"file": "static/html/index.html", "permission": 0},
"/results": "static/html/results.html", "/results": {"file": "static/html/results.html", "permission": 1},
"/archives": "static/html/archives.html", "/archives": {"file": "static/html/archives.html", "permission": 2},
"/favicon.ico": "static/img/favicon.ico", "/favicon.ico": {"file": "static/img/favicon.ico", "permission": 0},
"/login": "static/html/login.html", "/login": {"file": "static/html/login.html", "permission": 0},
"/register": "static/html/register.html" "/register": {"file": "static/html/register.html", "permission": 0},
} "/logout": {"file": "static/html/logout.html", "permission": 0},
"/unauthorised": {"file": "static/html/unauthorised.html", "permission": 0},
"/admin" : {"file": "static/html/admin.html", "permission": 3}
}

View File

@@ -1,4 +1,4 @@
{ {
"database_url": "sqlite:vote.db", "database_url": "sqlite:vote.db",
"bind_address": "127.0.0.1:8080" "bind_address": "127.0.0.1:8000"
} }

View File

@@ -1,44 +1,75 @@
use argon2::{ use argon2::{
password_hash::{ password_hash::{rand_core::OsRng, PasswordHash, PasswordHasher, PasswordVerifier, SaltString},
rand_core::OsRng,
PasswordHash, PasswordHasher, PasswordVerifier, SaltString,
},
Argon2, Argon2,
}; };
use bytes::Buf; use bytes::{Buf, Bytes};
use bytes::Bytes; use chrono::{DateTime, Days, NaiveTime, Utc};
use chrono::{DateTime, Days, Utc};
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
use daemonize::Daemonize; use daemonize::Daemonize;
use futures;
use http_body_util::{BodyExt, Full}; use http_body_util::{BodyExt, Full};
use hyper::body::{Body, Incoming}; use hyper::{
use hyper::header::{LOCATION, SET_COOKIE}; body::{Body as HyperBody, Frame, Incoming},
use hyper::server::conn::http1; header::{COOKIE, SET_COOKIE},
use hyper::service::service_fn; server::conn::http1,
use hyper::{Error, Method, Request, Response, StatusCode}; service::service_fn,
Error, Method, Request, Response, StatusCode,
};
use hyper_util::rt::{TokioIo, TokioTimer}; use hyper_util::rt::{TokioIo, TokioTimer};
use rand::distributions::{Alphanumeric, DistString}; use rand::distributions::{Alphanumeric, DistString};
use serde::{Deserialize, Serialize}; use serde::{de::DeserializeOwned, Deserialize, Serialize};
use serde_json::{from_reader, Value}; use serde_json::{from_reader, Value};
use sqlx::sqlite::SqlitePool; use sqlx::sqlite::SqlitePool;
use std::collections::HashMap; use std::collections::HashMap;
use std::convert::Infallible;
use std::env; use std::env;
use std::fs::File; use std::fs::File;
use std::io::Read; use std::io::Read;
use std::net::SocketAddr; use std::net::SocketAddr;
use std::pin::Pin;
use std::str::FromStr; use std::str::FromStr;
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
use std::task::{Context, Poll};
use std::time::SystemTime; use std::time::SystemTime;
use tokio::net::TcpListener; use tokio::net::TcpListener;
enum Body {
Full(Full<Bytes>),
Empty,
}
impl Body {
fn new<T>(data: T) -> Self
where
Bytes: From<T>,
{
Body::Full(Full::new(Bytes::from(data)))
}
}
impl HyperBody for Body {
type Data = Bytes;
type Error = Infallible;
fn poll_frame(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<Option<Result<Frame<Self::Data>, Self::Error>>> {
match &mut *self.get_mut() {
Self::Full(incoming) => Pin::new(incoming).poll_frame(cx),
Self::Empty => Poll::Ready(None),
}
}
}
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
struct Player { struct Player {
id: i64, id: i64,
name: String, name: String,
} }
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize, Debug)]
struct Vote { struct Vote {
id: Option<i64>,
submit_date: Option<String>,
plus_player_id: i64, plus_player_id: i64,
plus_nickname: String, plus_nickname: String,
plus_reason: String, plus_reason: String,
@@ -65,39 +96,71 @@ struct Settings {
database_url: String, database_url: String,
bind_address: String, bind_address: String,
} }
async fn service(req: Request<Incoming>, db: Arc<Mutex<SqlitePool>>) -> Result<Response<Full<Bytes>>, Error> {
match req.method() { async fn service(
&Method::GET => { get(req, db).await } req: Request<Incoming>,
&Method::POST => { post(req, db).await } db: Arc<Mutex<SqlitePool>>,
_ => { Ok(Response::builder().status(StatusCode::IM_A_TEAPOT).body(Full::new(Bytes::new())).unwrap()) } ) -> Result<Response<Body>, Error> {
match *req.method() {
Method::GET => get(req, db).await,
Method::POST => post(req, db).await,
_ => Ok(Response::builder()
.status(StatusCode::IM_A_TEAPOT)
.body(Body::Empty)
.unwrap()),
} }
} }
async fn get(req: Request<Incoming>, db: Arc<Mutex<SqlitePool>>) -> Result<Response<Full<Bytes>>, Error> { async fn get(req: Request<Incoming>, db: Arc<Mutex<SqlitePool>>) -> Result<Response<Body>, Error> {
let path = req.uri().path(); let path = req.uri().path();
if path.starts_with("/static") { if path.starts_with("/static") {
get_file(path).await get_file(path).await
} else if path.starts_with("/data") { } else if path.starts_with("/data") {
get_data(path, &req, db).await get_data(path, &req, db).await
} else if path.starts_with("/logout") { } else if path.starts_with("/admin") {
logout().await get_admin(&req, path, db).await
} else { } else {
get_page(path).await get_page(&req, path, db).await
} }
} }
async fn get_page(path: &str) -> Result<Response<Full<Bytes>>, Error> { async fn get_page(
let mut routes = env::current_dir().expect("Could not get app directory (Required to get routes)"); req: &Request<Incoming>,
path: &str,
db: Arc<Mutex<SqlitePool>>,
) -> Result<Response<Body>, Error> {
let mut routes =
env::current_dir().expect("Could not get app directory (Required to get routes)");
routes.push("routes.json"); routes.push("routes.json");
let file = File::open(routes).expect("Could not open routes file."); let file = File::open(routes).expect("Could not open routes file.");
let map: Value = from_reader(file).expect("Could not parse routes, please verify syntax."); let map: Value = from_reader(file).expect("Could not parse routes, please verify syntax.");
match &map[path] { match map.get(path) {
Value::String(s) => get_file(&s).await, Some(Value::Object(s)) => {
_ => not_found().await let authorised = is_authorised(
req,
db,
s.get("permission").unwrap().as_u64().unwrap() as u8,
)
.await;
if authorised {
get_file(s.get("file").unwrap().as_str().unwrap()).await
} else {
get_file(
map.get("/unauthorised")
.unwrap()
.get("file")
.unwrap()
.as_str()
.unwrap(),
)
.await
}
}
_ => not_found().await,
} }
} }
async fn get_file(mut path: &str) -> Result<Response<Full<Bytes>>, Error> { async fn get_file(mut path: &str) -> Result<Response<Body>, Error> {
let mut file_path = env::current_dir().expect("Could not get app directory."); let mut file_path = env::current_dir().expect("Could not get app directory.");
if path.starts_with(r"/") { if path.starts_with(r"/") {
path = path.strip_prefix(r"/").unwrap(); path = path.strip_prefix(r"/").unwrap();
@@ -112,58 +175,79 @@ async fn get_file(mut path: &str) -> Result<Response<Full<Bytes>>, Error> {
"js" => "text/javascript", "js" => "text/javascript",
"html" => "text/html", "html" => "text/html",
"css" => "text/css", "css" => "text/css",
_ => "" _ => "",
}; };
Ok(Response::builder().header("content-type", content_type).body(Full::new(Bytes::from(buf))).unwrap()) Ok(Response::builder()
.header("content-type", content_type)
.body(Body::new(buf))
.unwrap())
} }
Err(_) => not_found().await Err(_) => not_found().await,
} }
} }
async fn get_data(path: &str, req: &Request<Incoming>, db: Arc<Mutex<SqlitePool>>) -> Result<Response<Full<Bytes>>, Error> { async fn get_data(
path: &str,
req: &Request<Incoming>,
db: Arc<Mutex<SqlitePool>>,
) -> Result<Response<Body>, Error> {
let pool = db.clone().lock().unwrap().clone(); let pool = db.clone().lock().unwrap().clone();
match path { match path {
"/data/players" => { "/data/players" => {
let items = sqlx::query!(r#"SELECT * FROM players"#).fetch_all(&pool).await.unwrap(); let items = sqlx::query!(r#"SELECT * FROM players"#)
let players: Vec<Player> = items.iter().map(|x| Player { id: x.id, name: x.name.clone() }).collect(); .fetch_all(&pool)
Ok(Response::new(Full::new(Bytes::from(serde_json::to_string(&players).unwrap())))) .await
.unwrap();
let players: Vec<Player> = items
.iter()
.map(|x| Player {
id: x.id,
name: x.name.clone(),
})
.collect();
Ok(Response::new(Body::new(
serde_json::to_string(&players).unwrap(),
)))
} }
"/data/votes" => { "/data/votes" => {
let votes = get_votes(req, db).await; let votes = get_votes(req, db).await;
Ok(Response::new(Full::new(Bytes::from(serde_json::to_string(&votes).unwrap())))) Ok(Response::new(Body::new(
serde_json::to_string(&votes).unwrap(),
)))
} }
"/data/results" => { "/data/results" => {
let votes = get_votes(req, db).await; let votes = get_votes(req, db).await;
let ids: Vec<(i64, i64)> = votes.iter().map(|x| (x.plus_player_id, x.minus_player_id)).collect(); let ids: Vec<(i64, i64)> = votes
.iter()
.map(|x| (x.plus_player_id, x.minus_player_id))
.collect();
let mut plus_results: HashMap<i64, i64> = HashMap::new(); let mut plus_results: HashMap<i64, i64> = HashMap::new();
let mut minus_results: HashMap<i64, i64> = HashMap::new(); let mut minus_results: HashMap<i64, i64> = HashMap::new();
let _ = ids.iter().for_each(|x| { ids.iter().for_each(|x| {
let plus_id = x.0; let plus_id = x.0;
if !plus_results.contains_key(&plus_id) { plus_results.entry(plus_id).or_insert(0);
plus_results.insert(plus_id, 0);
}
*plus_results.get_mut(&plus_id).unwrap() += 1; *plus_results.get_mut(&plus_id).unwrap() += 1;
let minus_id = x.1; let minus_id = x.1;
if !minus_results.contains_key(&minus_id) { minus_results.entry(minus_id).or_insert(0);
minus_results.insert(minus_id, 0);
}
*minus_results.get_mut(&minus_id).unwrap() += 1; *minus_results.get_mut(&minus_id).unwrap() += 1;
}); });
let mut plus_results: Vec<(i64, i64)> = plus_results.into_iter().collect(); let mut plus_results: Vec<(i64, i64)> = plus_results.into_iter().collect();
let mut minus_results: Vec<(i64, i64)> = minus_results.into_iter().collect(); let mut minus_results: Vec<(i64, i64)> = minus_results.into_iter().collect();
plus_results.sort_by(|a, b| { b.1.cmp(&a.1) }); plus_results.sort_by(|a, b| b.1.cmp(&a.1));
minus_results.sort_by(|a, b| { b.1.cmp(&a.1) }); minus_results.sort_by(|a, b| b.1.cmp(&a.1));
let sorted_results = vec![plus_results, minus_results]; let sorted_results = vec![plus_results, minus_results];
Ok(Response::new(Full::new(Bytes::from(serde_json::to_string(&sorted_results).unwrap())))) Ok(Response::new(Body::new(
serde_json::to_string(&sorted_results).unwrap(),
)))
} }
_ => not_found().await _ => not_found().await,
} }
} }
@@ -173,55 +257,96 @@ async fn get_votes(req: &Request<Incoming>, db: Arc<Mutex<SqlitePool>>) -> Vec<V
let date = match headers.get("Date-to-fetch") { let date = match headers.get("Date-to-fetch") {
Some(date) => { Some(date) => {
let date = date.to_str().unwrap(); let date = date.to_str().unwrap();
let parsed_date = date.parse::<i64>(); if let Ok(parsed_date) = date.parse::<i64>() {
if parsed_date.is_err() { DateTime::from_timestamp_millis(parsed_date)
None
} else { } else {
DateTime::from_timestamp_millis(parsed_date.unwrap()) None
} }
} }
None => Some(DateTime::from(SystemTime::now())) None => Some(DateTime::from(SystemTime::now())),
}; };
if date.is_none() { if date.is_none() {
return Vec::new(); return Vec::new();
} }
let items = futures::executor::block_on(async move { let formatted_date = format!("{}", date.unwrap().format("%d/%m/%Y"));
let formatted_date = format!("{}", date.unwrap().format("%d/%m/%Y")); sqlx::query_as!(
sqlx::query!(r#"SELECT * FROM votes WHERE submit_date = ?1 ORDER BY id"#, formatted_date).fetch_all(&pool).await.unwrap() Vote,
}); r#"SELECT * FROM votes WHERE submit_date = ?1 ORDER BY id"#,
items.iter().map(|x| Vote { formatted_date
plus_player_id: x.plus_player_id, )
plus_nickname: x.plus_nickname.clone(), .fetch_all(&pool)
plus_reason: x.plus_reason.clone(), .await
minus_player_id: x.minus_player_id, .unwrap()
minus_nickname: x.minus_nickname.clone(),
minus_reason: x.minus_reason.clone(),
}).collect()
} }
async fn post(req: Request<Incoming>, db: Arc<Mutex<SqlitePool>>) -> Result<Response<Full<Bytes>>, Error> { async fn get_admin(
let path = req.uri().path(); req: &Request<Incoming>,
path: &str,
db: Arc<Mutex<SqlitePool>>,
) -> Result<Response<Body>, Error> {
let authorised = is_authorised(req, db.clone(), 3).await;
if !authorised {
return get_page(req, "/unauthorised", db).await;
}
match path { match path {
"/vote" => { "/admin" => get_page(req, path, db).await,
post_vote(req, db).await "/admin/users" => {
let pool = db.clone().lock().unwrap().clone();
let users = sqlx::query!(r#"SELECT id, username, permissions FROM users"#)
.fetch_all(&pool)
.await
.unwrap();
let users: Vec<(i64, String, i64)> = users
.iter()
.map(|x| (x.id, x.username.clone(), x.permissions))
.collect();
let stringed = serde_json::to_string(&users).unwrap_or("".to_string());
Ok(Response::builder().body(Body::new(stringed)).unwrap())
} }
"/login" => { "/admin/players" => {
login(req, db).await let pool = db.clone().lock().unwrap().clone();
let players = sqlx::query_as!(Player, r#"SELECT id, name FROM players"#)
.fetch_all(&pool)
.await
.unwrap();
let stringed = serde_json::to_string(&players).unwrap_or("".to_string());
Ok(Response::builder().body(Body::new(stringed)).unwrap())
} }
"/register" => { "/admin/votes" => {
register(req, db).await let pool = db.clone().lock().unwrap().clone();
} let votes = sqlx::query_as!(Vote, r#"SELECT * FROM votes"#)
_ => { .fetch_all(&pool)
not_found().await .await
.unwrap();
let stringed = serde_json::to_string(&votes).unwrap_or("".to_string());
Ok(Response::builder().body(Body::new(stringed)).unwrap())
} }
_ => not_found().await,
} }
} }
async fn post_vote(req: Request<Incoming>, db: Arc<Mutex<SqlitePool>>) -> Result<Response<Full<Bytes>>, Error> { async fn post(req: Request<Incoming>, db: Arc<Mutex<SqlitePool>>) -> Result<Response<Body>, Error> {
let body = req.into_body().collect().await?; let path = req.uri().path();
let data: Result<Vote, serde_json::Error> = from_reader(body.aggregate().reader()); if path.starts_with("/admin") {
if data.is_err() { return post_admin(req, db).await;
return Ok(Response::builder().status(StatusCode::BAD_REQUEST).body(Full::new(Bytes::from("Bad Request"))).unwrap()); }
match path {
"/vote" => post_vote(req, db).await,
"/login" => login(req, db).await,
"/register" => register(req, db).await,
"/logout" => logout().await,
"/player" => post_player(req, db).await,
_ => not_found().await,
}
}
async fn post_vote(
req: Request<Incoming>,
db: Arc<Mutex<SqlitePool>>,
) -> Result<Response<Body>, Error> {
let data = req_json::<Vote>(req).await;
if data.is_none() {
return bad_request().await;
} }
let vote = data.unwrap(); let vote = data.unwrap();
let timestamp: DateTime<Utc> = DateTime::from(SystemTime::now()); let timestamp: DateTime<Utc> = DateTime::from(SystemTime::now());
@@ -238,24 +363,239 @@ async fn post_vote(req: Request<Incoming>, db: Arc<Mutex<SqlitePool>>) -> Result
vote.minus_reason, vote.minus_reason,
formatted).execute(&mut *conn).await; formatted).execute(&mut *conn).await;
if result.is_err() { if result.is_err() {
return Ok(Response::builder().status(StatusCode::INTERNAL_SERVER_ERROR).body(Full::new(Bytes::from("Internet Error"))).unwrap()); return Ok(Response::builder()
.status(StatusCode::INTERNAL_SERVER_ERROR)
.body(Body::Empty)
.unwrap());
} }
Ok(Response::builder().body(Full::new(Bytes::new())).unwrap()) let date: DateTime<Utc> = DateTime::from(SystemTime::now());
let date = date.checked_add_days(Days::new(1)).unwrap();
let date = date
.with_time(NaiveTime::from_hms_opt(0, 0, 0).unwrap())
.unwrap();
Ok(Response::builder()
.header(
SET_COOKIE,
format!(
"hasvoted=true; Expires={}; Secure; SameSite=Strict",
date.to_rfc2822()
),
)
.body(Body::Empty)
.unwrap())
} }
async fn login(req: Request<Incoming>, db: Arc<Mutex<SqlitePool>>) -> Result<Response<Full<Bytes>>, Error> { async fn post_player(
req: Request<Incoming>,
db: Arc<Mutex<SqlitePool>>,
) -> Result<Response<Body>, Error> {
let data = req_json::<Value>(req).await;
if data.is_none() {
return bad_request().await;
}
let data = data.unwrap();
let name = data.get("name").unwrap().as_str().unwrap();
let pool = db.clone().lock().unwrap().clone();
let mut conn = pool.acquire().await.unwrap();
if let Ok(Some(player)) = sqlx::query!(r#"SELECT * FROM players WHERE name = ?1"#, name)
.fetch_optional(&pool)
.await
{
let player = Player {
id: player.id,
name: player.name,
};
return Ok(Response::builder()
.body(Body::new(serde_json::to_string(&player).unwrap()))
.unwrap());
}
let r = sqlx::query!(
r#"INSERT INTO players (name) VALUES (?1) RETURNING id"#,
name
)
.fetch_one(&mut *conn)
.await;
if r.is_err() {
return Ok(Response::builder()
.status(StatusCode::INTERNAL_SERVER_ERROR)
.body(Body::Empty)
.unwrap());
}
let player = Player {
id: r.unwrap().id,
name: name.to_string(),
};
Ok(Response::builder()
.body(Body::new(serde_json::to_string(&player).unwrap()))
.unwrap())
}
async fn post_admin(
req: Request<Incoming>,
db: Arc<Mutex<SqlitePool>>,
) -> Result<Response<Body>, Error> {
let authorised = is_authorised(&req, db.clone(), 3).await;
if !authorised {
return get_page(&req, "/unauthorised", db).await;
}
let path = req.uri().path();
match path {
"/admin/edit/user" => match req_json::<Value>(req).await {
Some(Value::Object(user)) => {
let username = user.get("username");
let permissions = user.get("permissions");
let id = user.get("id");
if username.is_none() || permissions.is_none() || id.is_none() {
return bad_request().await;
}
let pool = db.clone().lock().unwrap().clone();
let mut conn = pool.acquire().await.unwrap();
let username = username.unwrap().as_str().unwrap();
let permissions = permissions.unwrap();
let id = id.unwrap();
let _ = sqlx::query!(
r#"UPDATE users SET username = ?1, permissions = ?2 WHERE id = ?3"#,
username,
permissions,
id
)
.execute(&mut *conn)
.await;
ok().await
}
_ => bad_request().await,
},
"/admin/delete/user" => match req_json::<Value>(req).await {
Some(Value::Object(user)) => {
let id = user.get("id");
if id.is_none() {
return bad_request().await;
}
let pool = db.clone().lock().unwrap().clone();
let mut conn = pool.acquire().await.unwrap();
let id = id.unwrap().as_i64().unwrap();
let _ = sqlx::query!(r#"DELETE FROM users WHERE id = ?1"#, id)
.execute(&mut *conn)
.await;
ok().await
}
_ => bad_request().await,
},
"/admin/edit/player" => match req_json::<Player>(req).await {
Some(player) => {
let pool = db.clone().lock().unwrap().clone();
let mut conn = pool.acquire().await.unwrap();
let _ = sqlx::query!(
r#"UPDATE players SET name = ?1 WHERE id = ?2"#,
player.name,
player.id
)
.execute(&mut *conn)
.await;
ok().await
}
_ => bad_request().await,
},
"/admin/new/player" => match req_json::<Value>(req).await {
Some(Value::Object(player)) => {
let name = player.get("name");
if name.is_none() {
return bad_request().await;
}
let pool = db.clone().lock().unwrap().clone();
let mut conn = pool.acquire().await.unwrap();
let name = name.unwrap().as_str().unwrap();
let _ = sqlx::query!(r#"INSERT INTO players (name) VALUES (?1)"#, name)
.execute(&mut *conn)
.await;
ok().await
}
_ => bad_request().await,
},
"/admin/delete/player" => match req_json::<Value>(req).await {
Some(Value::Object(player)) => {
let id = player.get("id");
if id.is_none() {
return bad_request().await;
}
let pool = db.clone().lock().unwrap().clone();
let mut conn = pool.acquire().await.unwrap();
let id = id.unwrap().as_i64().unwrap();
let _ = sqlx::query!(r#"DELETE FROM players WHERE id = ?1"#, id)
.execute(&mut *conn)
.await;
ok().await
}
_ => bad_request().await,
},
"/admin/edit/vote" => match req_json::<Vote>(req).await {
Some(vote) => {
if vote.id.is_none() || vote.submit_date.is_none() {
return bad_request().await;
}
let pool = db.clone().lock().unwrap().clone();
let _ = sqlx::query!(
r#"UPDATE votes
SET submit_date = ?1,
plus_player_id = ?2,
plus_nickname = ?3,
plus_reason = ?4,
minus_player_id = ?5,
minus_nickname = ?6,
minus_reason = ?7
WHERE id = ?8"#,
vote.submit_date,
vote.plus_player_id,
vote.plus_nickname,
vote.plus_reason,
vote.minus_player_id,
vote.minus_nickname,
vote.minus_reason,
vote.id
)
.execute(&pool)
.await;
ok().await
}
_ => bad_request().await,
},
"/admin/delete/vote" => match req_json::<Value>(req).await {
Some(Value::Object(vote)) => {
let id = vote.get("id");
if id.is_none() {
return bad_request().await;
}
let pool = db.clone().lock().unwrap().clone();
let mut conn = pool.acquire().await.unwrap();
let id = id.unwrap().as_i64().unwrap();
let _ = sqlx::query!(r#"DELETE FROM votes WHERE id = ?1"#, id)
.execute(&mut *conn)
.await;
ok().await
}
_ => bad_request().await,
},
_ => bad_request().await,
}
}
async fn login(
req: Request<Incoming>,
db: Arc<Mutex<SqlitePool>>,
) -> Result<Response<Body>, Error> {
let body = req.into_body().collect().await; let body = req.into_body().collect().await;
let data: Result<Login, serde_json::Error> = from_reader(body?.aggregate().reader()); let data: Result<Login, serde_json::Error> = from_reader(body?.aggregate().reader());
if data.is_err() { if data.is_err() {
return Ok(Response::builder().status(StatusCode::BAD_REQUEST).body(Full::new(Bytes::from("Bad Request"))).unwrap()); return bad_request().await;
} }
let data = data.unwrap(); let data = data.unwrap();
if !check_username(&data.username) { if !check_username(&data.username) {
return Ok(Response::builder().status(StatusCode::BAD_REQUEST).body(Full::new(Bytes::from("Bad Request"))).unwrap()); return bad_request().await;
} }
let pool = db.clone().lock().unwrap().clone(); let pool = db.clone().lock().unwrap().clone();
let mut conn = pool.acquire().await.unwrap(); let result = sqlx::query!(r#"SELECT * FROM users WHERE username=?1"#, data.username)
let result = sqlx::query!(r#"SELECT * FROM users WHERE username=?1"#, data.username).fetch_optional(&mut *conn).await; .fetch_optional(&pool)
.await;
match result { match result {
Ok(Some(user)) => { Ok(Some(user)) => {
let argon = Argon2::default(); let argon = Argon2::default();
@@ -264,65 +604,141 @@ async fn login(req: Request<Incoming>, db: Arc<Mutex<SqlitePool>>) -> Result<Res
Ok(()) => { Ok(()) => {
let date: DateTime<Utc> = DateTime::from(SystemTime::now()); let date: DateTime<Utc> = DateTime::from(SystemTime::now());
let date = date.checked_add_days(Days::new(7)).unwrap(); let date = date.checked_add_days(Days::new(7)).unwrap();
// With server side rendering, redirect here to "/"
Ok(Response::builder() Ok(Response::builder()
.header(SET_COOKIE, .header(
format!("token={}; Expires={}; Secure; HttpOnly; SameSite=Strict", user.token, date.to_rfc2822())) SET_COOKIE,
.body(Full::new(Bytes::from("Ok"))) format!(
"token={}; Expires={}; Secure; HttpOnly; SameSite=Strict",
user.token,
date.to_rfc2822()
),
)
.header(
SET_COOKIE,
format!(
"logged=true; Expires={}; Secure; SameSite=Strict",
date.to_rfc2822()
),
)
.body(Body::Empty)
.unwrap()) .unwrap())
} }
Err(_) => { Err(_) => bad_request().await,
Ok(Response::builder().status(StatusCode::BAD_REQUEST).body(Full::new(Bytes::from("Bad Request"))).unwrap())
}
} }
} }
Ok(None) => { Ok(None) => bad_request().await,
Ok(Response::builder().status(StatusCode::BAD_REQUEST).body(Full::new(Bytes::from("Bad Request"))).unwrap()) Err(_) => Ok(Response::builder()
.status(StatusCode::INTERNAL_SERVER_ERROR)
.body(Body::Empty)
.unwrap()),
}
}
async fn register(
req: Request<Incoming>,
db: Arc<Mutex<SqlitePool>>,
) -> Result<Response<Body>, Error> {
match req_json::<Login>(req).await {
None => Ok(Response::builder()
.status(StatusCode::BAD_REQUEST)
.body(Body::Empty)
.unwrap()),
Some(login) => {
if !check_username(&login.username) {
return Ok(Response::builder()
.status(StatusCode::BAD_REQUEST)
.body(Body::Empty)
.unwrap());
}
if !check_password(&login.password) {
return Ok(Response::builder()
.status(StatusCode::BAD_REQUEST)
.body(Body::Empty)
.unwrap());
}
let pool = db.clone().lock().unwrap().clone();
let mut conn = pool.acquire().await.unwrap();
let exists = sqlx::query!(r#"SELECT id FROM users WHERE username=?1"#, login.username)
.fetch_optional(&mut *conn)
.await;
if exists.unwrap().is_some() {
return Ok(Response::builder()
.status(StatusCode::BAD_REQUEST)
.body(Body::Empty)
.unwrap());
}
let argon2 = Argon2::default();
let hash = argon2
.hash_password(login.password.as_bytes(), &SaltString::generate(&mut OsRng))
.unwrap()
.to_string();
let mut token = Alphanumeric.sample_string(&mut OsRng, 256);
while let Ok(Some(_)) = sqlx::query!(r#"SELECT id FROM users WHERE token=?1"#, token)
.fetch_optional(&mut *conn)
.await
{
token = Alphanumeric.sample_string(&mut OsRng, 256);
}
let result = sqlx::query!(r#"INSERT INTO users ( username, saltyhash, permissions, token) VALUES ( ?1, ?2, ?3, ?4 )"#, login.username, hash, 0, token).execute(&mut *conn).await;
match result {
Ok(_) => Ok(Response::builder().body(Body::Empty).unwrap()),
Err(_) => Ok(Response::builder()
.status(StatusCode::INTERNAL_SERVER_ERROR)
.body(Body::Empty)
.unwrap()),
}
} }
Err(_) => { Ok(Response::builder().status(StatusCode::INTERNAL_SERVER_ERROR).body(Full::new(Bytes::from("Server Error"))).unwrap()) }
} }
} }
async fn register(req: Request<Incoming>, db: Arc<Mutex<SqlitePool>>) -> Result<Response<Full<Bytes>>, Error> { async fn logout() -> Result<Response<Body>, Error> {
let body = req.into_body().collect().await;
let data: Result<Login, serde_json::Error> = from_reader(body?.aggregate().reader());
if data.is_err() {
return Ok(Response::builder().status(StatusCode::BAD_REQUEST).body(Full::new(Bytes::from("Bad Request"))).unwrap());
}
let data = data.unwrap();
if !check_username(&data.username) {
return Ok(Response::builder().status(StatusCode::BAD_REQUEST).body(Full::new(Bytes::from("Bad Request"))).unwrap());
}
if !check_password(&data.password) {
return Ok(Response::builder().status(StatusCode::BAD_REQUEST).body(Full::new(Bytes::from("Bad Request"))).unwrap());
}
let pool = db.clone().lock().unwrap().clone();
let mut conn = pool.acquire().await.unwrap();
let exists = sqlx::query!(r#"SELECT id FROM users WHERE username=?1"#, data.username).fetch_optional(&mut *conn).await;
if exists.unwrap().is_some() {
return Ok(Response::builder().status(StatusCode::BAD_REQUEST).body(Full::new(Bytes::from("Bad Request"))).unwrap());
}
let argon2 = Argon2::default();
let hash = argon2.hash_password(data.password.as_bytes(), &SaltString::generate(&mut OsRng)).unwrap().to_string();
let token = Alphanumeric.sample_string(&mut OsRng, 256);
println!("{}", token);
let result = sqlx::query!(r#"INSERT INTO users ( username, saltyhash, permissions, token) VALUES ( ?1, ?2, ?3, ?4 )"#, data.username, hash, 0, token).execute(&mut *conn).await;
match result {
Ok(_) => Ok(Response::builder().body(Full::new(Bytes::from(""))).unwrap()),
Err(_) => { Ok(Response::builder().status(StatusCode::INTERNAL_SERVER_ERROR).body(Full::new(Bytes::from("Server Error"))).unwrap()) }
}
}
async fn logout() -> Result<Response<Full<Bytes>>, Error> {
let date: DateTime<Utc> = DateTime::from(SystemTime::now()); let date: DateTime<Utc> = DateTime::from(SystemTime::now());
Ok(Response::builder() Ok(Response::builder()
.status(StatusCode::SEE_OTHER) .header(
.header(LOCATION, "/") SET_COOKIE,
.header(SET_COOKIE, format!("token=''; Expires={}; Secure; HttpOnly; SameSite=Strict", date.to_rfc2822())) format!(
.body(Full::new(Bytes::from(""))).unwrap()) "token=''; Expires={}; Secure; HttpOnly; SameSite=Strict",
date.to_rfc2822()
),
)
.header(
SET_COOKIE,
format!(
"logged=false; Expires={}; Secure; HttpOnly; SameSite=Strict",
date.to_rfc2822()
),
)
.body(Body::Empty)
.unwrap())
} }
fn check_username(username: &String) -> bool { async fn is_authorised(req: &Request<Incoming>, db: Arc<Mutex<SqlitePool>>, level: u8) -> bool {
let cookies = req.headers().get(COOKIE);
let token = match cookies {
Some(cookies) => cookies
.to_str()
.unwrap_or("")
.split("; ")
.find(|x| x.starts_with("token="))
.unwrap_or("")
.strip_prefix("token=")
.unwrap_or(""),
None => "",
};
let pool = db.clone().lock().unwrap().clone();
let user = sqlx::query!(r#"SELECT permissions FROM users WHERE token=?1"#, token)
.fetch_optional(&pool)
.await;
match user {
Ok(Some(user)) => {
let perm = user.permissions as u8;
perm >= level
}
_ => matches!(level, 0),
}
}
fn check_username(username: &str) -> bool {
if username.len() > 21 { if username.len() > 21 {
return false; return false;
} }
@@ -334,7 +750,7 @@ fn check_username(username: &String) -> bool {
true true
} }
fn check_password(password: &String) -> bool { fn check_password(password: &str) -> bool {
// one symbol, 10 chars min, one capital letter, one number // one symbol, 10 chars min, one capital letter, one number
if password.len() < 10 { if password.len() < 10 {
return false; return false;
@@ -356,20 +772,47 @@ fn check_password(password: &String) -> bool {
up && num && sym up && num && sym
} }
async fn not_found() -> Result<Response<Full<Bytes>>, Error> { async fn not_found() -> Result<Response<Body>, Error> {
let mut file_path = env::current_dir().expect("Could not get app directory."); let mut file_path = env::current_dir().expect("Could not get app directory.");
file_path.push("static/html/404.html"); file_path.push("static/html/404.html");
let mut file = File::open(file_path).unwrap(); let mut file = File::open(file_path).unwrap();
let mut buf = Vec::new(); let mut buf = Vec::new();
file.read_to_end(&mut buf).unwrap(); file.read_to_end(&mut buf).unwrap();
Ok(Response::builder().status(StatusCode::NOT_FOUND).body(Full::new(Bytes::from(buf))).unwrap()) Ok(Response::builder()
.status(StatusCode::NOT_FOUND)
.body(Body::new(buf))
.unwrap())
}
async fn bad_request() -> Result<Response<Body>, Error> {
Ok(Response::builder()
.status(StatusCode::BAD_REQUEST)
.body(Body::Empty)
.unwrap())
}
async fn ok() -> Result<Response<Body>, Error> {
Ok(Response::builder().body(Body::Empty).unwrap())
}
async fn req_json<T>(req: Request<Incoming>) -> Option<T>
where
T: DeserializeOwned,
{
let body = req.into_body().collect().await.unwrap();
match from_reader(body.aggregate().reader()) {
Ok(val) => Some(val),
Err(_) => None,
}
} }
fn get_settings() -> Settings { fn get_settings() -> Settings {
let mut settings_path = env::current_dir().expect("Could not get app directory. (Required to read settings)"); let mut settings_path =
env::current_dir().expect("Could not get app directory. (Required to read settings)");
settings_path.push("settings.json"); settings_path.push("settings.json");
let settings_file = File::open(settings_path).expect("Could not open settings file, does it exists?"); let settings_file =
let settings: Settings = from_reader(settings_file).expect("Could not parse settings, please check syntax."); File::open(settings_path).expect("Could not open settings file, does it exists?");
let settings: Settings =
from_reader(settings_file).expect("Could not parse settings, please check syntax.");
settings settings
} }
@@ -398,26 +841,32 @@ fn main() {
#[tokio::main] #[tokio::main]
async fn run() { async fn run() {
let settings = get_settings(); let settings = get_settings();
let db_pool = Arc::new(Mutex::new(SqlitePool::connect(&settings.database_url).await.expect("Could not connect to database. Make sure the url is correct."))); let db_pool = Arc::new(Mutex::new(
let bind_address: SocketAddr = SocketAddr::from_str(&settings.bind_address).expect("Could not parse bind address."); SqlitePool::connect(&settings.database_url)
let listener = TcpListener::bind(bind_address).await.expect("Could not bind to address."); .await
.expect("Could not connect to database. Make sure the url is correct."),
));
let bind_address: SocketAddr =
SocketAddr::from_str(&settings.bind_address).expect("Could not parse bind address.");
let listener = TcpListener::bind(bind_address)
.await
.expect("Could not bind to address.");
loop { loop {
let (stream, _) = listener.accept().await.expect("Could not accept incoming stream."); let (stream, _) = listener
.accept()
.await
.expect("Could not accept incoming stream.");
let io = TokioIo::new(stream); let io = TokioIo::new(stream);
let db = db_pool.clone(); let db = db_pool.clone();
let service = service_fn( let service = service_fn(move |req| service(req, db.clone()));
move |req| { tokio::task::spawn(async move {
service(req, db.clone()) if let Err(err) = http1::Builder::new()
.timer(TokioTimer::new())
.serve_connection(io, service)
.await
{
println!("Failed to serve connection: {:?}", err);
} }
); });
tokio::task::spawn(
async move {
if let Err(err) = http1::Builder::new()
.timer(TokioTimer::new())
.serve_connection(io, service).await {
println!("Failed to serve connection: {:?}", err);
}
}
);
} }
} }

View File

@@ -119,3 +119,11 @@ body {
border: none; border: none;
border-radius: 5px; border-radius: 5px;
} }
.login {
position: fixed;
right: 0;
top: 0;
color: white;
padding: 10px;
}

View File

@@ -32,7 +32,7 @@ body {
transform: translate(-50%, calc(-50% - 60px)); transform: translate(-50%, calc(-50% - 60px));
background-color: #181926; background-color: #181926;
border-radius: 12px; border-radius: 12px;
width: 100%; width: calc(100dvw - 40px);
max-width: 500px; max-width: 500px;
height: fit-content; height: fit-content;
padding: 10px; padding: 10px;
@@ -55,7 +55,8 @@ body {
.sub > input { .sub > input {
margin-bottom: 5px; margin-bottom: 5px;
width: 300px; max-width: 300px;
width: 80dvw;
color: black; color: black;
} }

23
static/css/logout.css Normal file
View File

@@ -0,0 +1,23 @@
body {
background-color: #11111b;
padding:0;
margin:0;
}
h2 {
color: yellow;
}
p {
color: white;
}
a {
color: white;
}
.app {
width: fit-content;
margin: auto;
text-align: center;
}

7
static/css/register.css Normal file
View File

@@ -0,0 +1,7 @@
.error {
color: red;
}
.ok {
color: green;
}

View File

@@ -26,9 +26,8 @@ body {
} }
.app { .app {
margin: auto; margin: auto auto 60px;
margin-bottom: 60px; height: calc(100% + 60px);
height: calc(100dvh + 60px);
width: 100%; width: 100%;
max-width: 500px; max-width: 500px;
} }
@@ -76,3 +75,11 @@ body {
grid-column-start: 2; grid-column-start: 2;
grid-column-end: 3; grid-column-end: 3;
} }
.login {
position: fixed;
right: 0;
top: 0;
color: white;
padding: 10px;
}

View File

@@ -0,0 +1,16 @@
body {
background-color: #11111b;
text-align: center;
}
h1 {
color: red;
}
h3 {
color: yellow;
}
p, a {
color: white;
}

30
static/html/admin.html Normal file
View File

@@ -0,0 +1,30 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<base href="/" target="_top">
<script src="static/js/admin.js" defer="defer"></script>
<title>Administration</title>
</head>
<body>
<div>
<h1>Users</h1>
<p>id, username, permission</p>
<div id="users"></div>
</div>
<div>
<h1>Players</h1>
<p>id, name</p>
<div id="players"></div>
</div>
<div>
<h1>Votes</h1>
<h3 id="votes_number"></h3>
<p>id, submit_date, plus_id, plus_nickname, plus_reason, minus_id, minus_nickname, minus_reason</p>
<div id="votes"></div>
</div>
</body>
</html>

View File

@@ -15,6 +15,8 @@
<h1 id="app_title">Vote +</h1> <h1 id="app_title">Vote +</h1>
<label for="player_id">Pour qui votes tu?</label> <label for="player_id">Pour qui votes tu?</label>
<select id="player_id"></select> <select id="player_id"></select>
<label id="other_label" for="other" hidden="hidden">Autre:</label>
<input id="other" hidden="hidden">
<label for="nickname">As-tu un surnom à lui donner?</label> <label for="nickname">As-tu un surnom à lui donner?</label>
<input type="text" id="nickname"> <input type="text" id="nickname">
<label for="reason">Pourquoi votes-tu pour lui?</label> <label for="reason">Pourquoi votes-tu pour lui?</label>
@@ -34,5 +36,6 @@
<a href="/results">Résultats</a> <a href="/results">Résultats</a>
<a href="/archives">Archives</a> <a href="/archives">Archives</a>
</div> </div>
<a href="/login" class="login" id="login">Se connecter</a>
</body> </body>
</html> </html>

20
static/html/logout.html Normal file
View File

@@ -0,0 +1,20 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<base href="/" target="_top">
<script src="static/js/logout.js" defer="defer"></script>
<link rel="stylesheet" href="static/css/logout.css">
<title>Déconnection</title>
</head>
<body>
<div class="app">
<h2>Tu es désormais déconnecté.<br>Tu seras redirigé dans pas longtemps.</h2>
<p>Pas encore redirigé?</p>
<a href="/">Click ici pour être redirigé.</a>
</div>
</body>
</html>

View File

@@ -8,6 +8,7 @@
<base href="/" target="_top"> <base href="/" target="_top">
<script src="static/js/register.js" defer="defer"></script> <script src="static/js/register.js" defer="defer"></script>
<link rel="stylesheet" href="static/css/login.css"> <link rel="stylesheet" href="static/css/login.css">
<link rel="stylesheet" href="static/css/register.css">
<title>Register</title> <title>Register</title>
</head> </head>
<body> <body>
@@ -19,14 +20,24 @@
<br> <br>
<input id="username"> <input id="username">
<br> <br>
<p class="error" id="usernameNotice">Le nom d'utilisateur doit ne pas contenir de caractères spéciaux.</p>
<label for="password">Mot de passe:</label> <label for="password">Mot de passe:</label>
<br> <br>
<input id="password" type="password"> <input id="password" type="password">
<br> <p class="error" id="passwordNotice">Le mot de passe doit contenir plus de 8 caractères et doit contenir au minimum:
<br>
1 Majuscule
<br>
1 Symbole
<br>
1 Minuscule
<br>
1 Nombre
</p>
<label for="passwordConfirm">Confirmer le mot de passe:</label> <label for="passwordConfirm">Confirmer le mot de passe:</label>
<br> <br>
<input id="passwordConfirm" type="password"> <input id="passwordConfirm" type="password">
<br> <p class="error" id="passwordConfirmNotice">Les mots de passe doivent être les mêmes.</p>
<button id="register">Créer un compte.</button> <button id="register">Créer un compte.</button>
</div> </div>
<p>Tu as déjà un compte?</p> <p>Tu as déjà un compte?</p>

View File

@@ -11,6 +11,7 @@
<script src="/static/js/results.js" defer="defer"></script> <script src="/static/js/results.js" defer="defer"></script>
</head> </head>
<body> <body>
<a href="/login" class="login" id="login">Se connecter</a>
<div id="app" class="app"></div> <div id="app" class="app"></div>
<div id="footer" class="footer"> <div id="footer" class="footer">
<a href="/">Voter</a> <a href="/">Voter</a>

View File

@@ -0,0 +1,22 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<base href="/" target="_top">
<link rel="stylesheet" href="static/css/unauthorised.css">
<title>Non autorisé</title>
</head>
<body>
<div class="app" id="app">
<h1>Tu n'es pas autorisé à accèder à cette page.</h1>
<h3>Merci de bien vouloir te connecter.</h3>
<a href="/login">Se connecter</a>
<p>Tu n'as pas de compte?</p>
<a href="/register">Créer un compte</a>
<h3>Si c'est une erreur, contacte un administrateur.</h3>
</div>
</body>
</html>

123
static/js/admin.js Normal file
View File

@@ -0,0 +1,123 @@
async function run() {
let users = await fetch("/admin/users").then(r => r.json());
let usersDiv = document.getElementById("users");
for (let i = 0; i < users.length; i++) {
let item = document.createElement("div");
let id = document.createElement("p");
let username = document.createElement("input");
let permissions = document.createElement("input");
let edit = document.createElement("button");
let del = document.createElement("button");
edit.textContent = "Edit";
del.textContent = "Delete";
id.textContent = users[i][0];
username.value = users[i][1];
permissions.value = users[i][2];
item.style.display = "flex";
item.append(id, username, permissions, edit, del);
usersDiv.appendChild(item);
edit.addEventListener("click", async () => {
await fetch("/admin/edit/user", {method: "POST", body: JSON.stringify({"id": users[i][0], "username": username.value, "permissions": parseInt(permissions.value)})});
window.location.reload();
})
del.addEventListener("click", async () => {
await fetch("/admin/delete/user", {method:"POST", body: JSON.stringify({"id": users[i][0]})});
window.location.reload();
})
}
let players = await fetch("/admin/players").then(r => r.json());
let playersDiv = document.getElementById("players");
for (let i=0; i<players.length; i++) {
let item = document.createElement("div");
let id = document.createElement("p");
let name = document.createElement("input");
let edit = document.createElement("button");
let del = document.createElement("button");
edit.textContent = "Edit";
del.textContent = "Delete";
id.textContent = players[i]["id"];
name.value = players[i]["name"];
item.style.display = "flex";
item.append(id, name, edit, del);
playersDiv.appendChild(item);
edit.addEventListener("click", async () => {
await fetch("/admin/edit/player", {method: "POST", body: JSON.stringify({"id": players[i]["id"], "name": name.value})});
window.location.reload();
})
del.addEventListener("click", async () => {
await fetch("/admin/delete/player", {method:"POST", body: JSON.stringify({"id": players[i]["id"]})});
window.location.reload();
})
}
let newPlayer = document.createElement("button");
newPlayer.textContent = "Add Player";
newPlayer.addEventListener("click", () => {
let item = document.createElement("div");
let name = document.createElement("input");
let save = document.createElement("button");
save.textContent = "Save";
item.append(name, save);
playersDiv.appendChild(item);
save.addEventListener("click", async () => {
await fetch("/admin/new/player", {method:"POST", body: JSON.stringify({"name": name.value})})
window.location.reload();
})
})
playersDiv.parentNode.append(newPlayer);
let votes = await fetch("/admin/votes").then(r => r.json());
let today = document.getElementById("votes_number");
let count = await fetch("/data/votes").then(r => r.json());
today.textContent = `Aujourd'hui il y a ${count.length} votes`;
let votesDiv = document.getElementById("votes");
for (let i=0; i<votes.length; i++) {
let vote = votes[i];
let item = document.createElement("div");
item.style.display = "flex";
let id = document.createElement("p");
id.textContent = vote["id"];
let submit_date = document.createElement("input");
submit_date.value = vote["submit_date"];
let plus_id = document.createElement("input");
plus_id.type = "number";
plus_id.value = vote["plus_player_id"];
let plus_nickname = document.createElement("input");
plus_nickname.value = vote["plus_nickname"];
let plus_reason = document.createElement("input");
plus_reason.value = vote["plus_reason"];
let minus_id = document.createElement("input");
minus_id.type = "number";
minus_id.value = vote["minus_player_id"];
let minus_nickname = document.createElement("input");
minus_nickname.value = vote["minus_nickname"];
let minus_reason = document.createElement("input");
minus_reason.value = vote["minus_reason"];
let edit = document.createElement("button");
edit.textContent = "Edit";
let del = document.createElement("button");
del.textContent = "Delete";
item.append(id,submit_date,plus_id,plus_nickname,plus_reason,minus_id,minus_nickname,minus_reason, edit, del);
votesDiv.append(item);
edit.addEventListener("click", async () => {
await fetch("/admin/edit/vote", {method: "POST", body: JSON.stringify({"id": votes[i]["id"],
"submit_date": submit_date.value,
"plus_player_id": parseInt(plus_id.value),
"plus_nickname": plus_nickname.value,
"plus_reason": plus_reason.value,
"minus_player_id": parseInt(minus_id.value),
"minus_nickname": minus_nickname.value,
"minus_reason": minus_reason.value})});
window.location.reload();
})
del.addEventListener("click", async () => {
await fetch("/admin/delete/vote", {method:"POST", body: JSON.stringify({"id": votes[i]["id"]})});
window.location.reload();
})
}
}
let _ = run();

View File

@@ -1,4 +1,5 @@
let vote = { let vote = {
submit_date: null,
plus_player_id: null, plus_player_id: null,
plus_nickname: "", plus_nickname: "",
plus_reason: "", plus_reason: "",
@@ -11,7 +12,7 @@ let current_page = 0;
async function main() { async function main() {
if (read_cookie()) { if (read_cookie()) {
showMessage("Merci pour ton vote!", "Ton vote a bien été prit en compte.", false, "info"); showMessage("Merci pour ton vote!", "Ton vote a bien été pris en compte.", false, "info");
return; return;
} }
let players = await fetch("/data/players").then(r => r.json()); let players = await fetch("/data/players").then(r => r.json());
@@ -24,10 +25,25 @@ async function main() {
option.textContent = x["name"]; option.textContent = x["name"];
select.append(option); select.append(option);
}) })
let other = document.createElement("option");
other.value = "other";
other.textContent = "Autre";
select.append(other);
select.value = null; select.value = null;
nickname.value = ""; nickname.value = "";
reason.value = ""; reason.value = "";
select.addEventListener("change", () => { select.addEventListener("change", () => {
if (select.value === "other") {
let other = document.getElementById("other");
other.hidden = false;
let otherLabel = document.getElementById("other_label");
otherLabel.hidden = false;
} else {
let other = document.getElementById("other");
other.hidden = true;
let otherLabel = document.getElementById("other_label");
otherLabel.hidden = true;
}
if (current_page) { if (current_page) {
vote.minus_player_id = parseInt(select.value); vote.minus_player_id = parseInt(select.value);
} else { } else {
@@ -59,12 +75,28 @@ async function main() {
true, "warning"); true, "warning");
return; return;
} }
if (select.value === "other") {
let otherInput = document.getElementById("other");
if (otherInput.value === "") {
showMessage("Tu as sélectionné autre mais tu n'as pas donné de nom", "N'oublie pas de soit sélectioner une personne ou de mettre un nom dans ''Autre:''.", true, "warning");
return;
}
let otherInputLabel = document.getElementById("other_label");
let player = await fetch("/player", {method: "post", body: JSON.stringify({"name": otherInput.value})}).then(r => r.json());
let option = document.createElement("option");
option.value = player["id"];
option.textContent = player["name"];
select.insertBefore(option, other);
vote.minus_player_id = parseInt(player["id"]);
otherInput.value = "";
otherInput.hidden = true;
otherInputLabel.hidden = true;
}
if (await fetch("/vote", { if (await fetch("/vote", {
method: "post", body: JSON.stringify(vote) method: "post", body: JSON.stringify(vote)
}) })
.then(r => r.status) === 200) { .then(r => r.status) === 200) {
set_cookie(); showMessage("Merci pour ton vote!", "Ton vote a bien été pris en compte.", false, "info");
showMessage("Merci pour ton vote!", "Ton vote a bien été prit en compte.", false, "info");
} }
console.log(vote); console.log(vote);
} else { } else {
@@ -76,6 +108,23 @@ async function main() {
} }
rightButton.textContent = "À voté!"; rightButton.textContent = "À voté!";
title.textContent = "Vote -"; title.textContent = "Vote -";
if (select.value === "other") {
let otherInput = document.getElementById("other");
if (otherInput.value === "") {
showMessage("Tu as sélectionné autre mais tu n'as pas donné de nom", "N'oublie pas de soit sélectioner une personne ou de mettre un nom dans ''Autre:''.", true, "warning");
return;
}
let otherInputLabel = document.getElementById("other_label");
let player = await fetch("/player", {method: "post", body: JSON.stringify({"name": otherInput.value})}).then(r => r.json());
let option = document.createElement("option");
option.value = player["id"];
option.textContent = player["name"];
select.insertBefore(option, other);
vote.plus_player_id = parseInt(player["id"]);
otherInput.value = "";
otherInput.hidden = true;
otherInputLabel.hidden = true;
}
current_page = 1; current_page = 1;
leftButton.hidden = false; leftButton.hidden = false;
select.value = vote.minus_player_id; select.value = vote.minus_player_id;
@@ -111,16 +160,13 @@ function showMessage(title, description, canBeDismissed, type) {
}) })
} }
function set_cookie() {
let date = new Date(Date.now());
date.setDate(date.getDate() + 1);
date.setHours(0, 0,0);
console.log(date);
document.cookie = `hasvoted=true; expires=${date.toUTCString()}; path=/`;
}
function read_cookie() { function read_cookie() {
return document.cookie.includes("hasvoted=true"); return document.cookie.includes("hasvoted=true");
} }
let login = document.getElementById("login");
let logged = document.cookie.includes("logged=true");
login.textContent = logged ? "Se déconnecter" : "Se connecter";
login.href = logged ? "/logout" : "/login";
let _ = main(); let _ = main();

4
static/js/logout.js Normal file
View File

@@ -0,0 +1,4 @@
setTimeout(async () => {
await fetch("/logout", {method: "POST"});
window.location.href = "/";
}, 3000);

View File

@@ -1,11 +1,58 @@
async function register() {} async function register() {
let username = document.getElementById("username").value;
let password = document.getElementById("password").value;
let passwordConfirm = document.getElementById("passwordConfirm").value;
if (!check_username(username) || !check_password(password) || password !== passwordConfirm) {
return;
}
let status = await fetch("/register", {method: "POST", body: JSON.stringify({"username": username, "password": password})}).then(r => r.status)
if (status === 200) {
window.location.href = "/login";
} else if (status === 400) {
document.getElementById("message").textContent = "Cet utilisateur existe déjà!"
}
}
function check_username() {} function check_username(username) {
return username.match(/^[0-9a-zA-Z]+$/);
}
function check_password() {} function check_password(password) {
return password.match(/^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[!-/:-@[-`{-~]).{8,}$/)
}
let button = document.getElementById("register"); let button = document.getElementById("register");
button.addEventListener("click", () => { button.addEventListener("click", () => {
let _ = register(); let _ = register();
}) })
let username = document.getElementById("username");
username.addEventListener("input", () => {
if (check_username(username.value)) {
document.getElementById("usernameNotice").className = "ok";
} else {
document.getElementById("usernameNotice").className = "error";
}
})
let password = document.getElementById("password");
password.addEventListener("input", () => {
if (check_password(password.value)) {
document.getElementById("passwordNotice").className = "ok";
} else {
document.getElementById("passwordNotice").className = "error";
}
})
let passwordConfirm = document.getElementById("passwordConfirm");
passwordConfirm.addEventListener("input", () => {
if (passwordConfirm.value === password.value) {
document.getElementById("passwordConfirmNotice").className = "ok";
} else {
document.getElementById("passwordConfirmNotice").className = "error";
}
})

View File

@@ -23,7 +23,12 @@ function show_plus(id, votes, players) {
const app = document.getElementById("app"); const app = document.getElementById("app");
app.innerHTML = ""; app.innerHTML = "";
let vote = votes[id]; let vote = votes[id];
let player = players[vote["plus_player_id"] - 1]["name"]; let player = "?";
for (let i = 0; i < players.length; i++) {
if (players[i].id === vote["plus_player_id"]) {
player = players[i]["name"];
}
}
let nickname = vote["plus_nickname"]; let nickname = vote["plus_nickname"];
let reason = vote["plus_reason"]; let reason = vote["plus_reason"];
let minus = document.createElement("button"); let minus = document.createElement("button");
@@ -58,7 +63,12 @@ function show_minus(id, votes, players) {
let vote = votes[id]; let vote = votes[id];
let nickname = vote["minus_nickname"]; let nickname = vote["minus_nickname"];
let reason = vote["minus_reason"]; let reason = vote["minus_reason"];
let player = players[vote["minus_player_id"] - 1]["name"]; let player = "?";
for (let i = 0; i < players.length; i++) {
if (players[i].id === vote["minus_player_id"]) {
player = players[i]["name"];
}
}
let next = document.createElement("button"); let next = document.createElement("button");
if (id === votes.length - 1) { if (id === votes.length - 1) {
next.textContent = "Résultats"; next.textContent = "Résultats";
@@ -112,7 +122,12 @@ async function show_results(players) {
let counter = 0; let counter = 0;
for (let i = 0; i < plus.length; i++) { for (let i = 0; i < plus.length; i++) {
let p = plus[i]; let p = plus[i];
let player = players[p[0] - 1]["name"]; let player = "?";
for (let i = 0; i < players.length; i++) {
if (players[i].id === p[0]) {
player = players[i]["name"];
}
}
let score = p[1]; let score = p[1];
if (prev_score == null || score < prev_score) { if (prev_score == null || score < prev_score) {
counter += 1; counter += 1;
@@ -134,7 +149,12 @@ async function show_results(players) {
counter = 0; counter = 0;
for (let i = 0; i < minus.length; i++) { for (let i = 0; i < minus.length; i++) {
let p = minus[i]; let p = minus[i];
let player = players[p[0] - 1]["name"]; let player = "?";
for (let i = 0; i < players.length; i++) {
if (players[i].id === p[0]) {
player = players[i]["name"];
}
}
let score = p[1]; let score = p[1];
if (prev_score == null || score < prev_score) { if (prev_score == null || score < prev_score) {
counter += 1; counter += 1;
@@ -149,4 +169,9 @@ async function show_results(players) {
} }
} }
let login = document.getElementById("login");
let logged = document.cookie.includes("logged=true");
login.textContent = logged ? "Se déconnecter" : "Se connecter";
login.href = logged ? "/logout" : "/login";
let _ = run(); let _ = run();