diff --git a/.gitignore b/.gitignore index 102a49f..8fc794b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,13 +1,5 @@ /target Cargo.lock -/database/database.sqlite -/NEA_Screenshots + .DS_Store -.gitignore -/.database -start.sh -TODO.md -/server -/servers_old -Rocket.toml -/.hidden \ No newline at end of file +.env \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index 8fd44b1..53c8617 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,10 +4,10 @@ version = "0.1.0" edition = "2021" [dependencies] -rusqlite = "0.28.0" rand = "0.8.5" sha256 = "1.1.1" +sqlx = { version = "0.7.1", features = ["macros", "sqlite", "runtime-tokio"] } [dependencies.rocket] version = "0.5.0-rc.2" -features = ["json", "tls"] \ No newline at end of file +features = ["json", "tls"] diff --git a/TODO.md b/TODO.md deleted file mode 100644 index 7af64fe..0000000 --- a/TODO.md +++ /dev/null @@ -1,2 +0,0 @@ -### Backend -- [ ] hello there \ No newline at end of file diff --git a/database/dev/database.sqlite b/database/dev/database.sqlite new file mode 100755 index 0000000..915d40c Binary files /dev/null and b/database/dev/database.sqlite differ diff --git a/done.md b/done.md new file mode 100644 index 0000000..77fc017 --- /dev/null +++ b/done.md @@ -0,0 +1 @@ +- Migrated to sqlx \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index e7f21ad..c7b86d9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -45,7 +45,7 @@ mod typing; use crate::servers::server::{server, server_info}; use crate::typing::leaderboard::leaderboard; -use crate::typing::sql::*; +use crate::typing::sql::Database; use crate::typing::test::{create_test, new_test}; use crate::typing::user::{get_tests, login, sign_up}; @@ -54,14 +54,14 @@ use crate::typing::user::{get_tests, login, sign_up}; /// Test api route that returns hello world. /// Acessible from http://url/test #[get("/")] -fn test() -> String { - String::from("Hello World!") +fn test() -> &'static str { + "Hello World! I'm A rocket Webserver" } /// The main function which builds and launches the /// webserver with all appropriate routes and fileservers #[launch] -fn rocket() -> Rocket { +async fn rocket() -> Rocket { rocket::build() .attach(CORS) // testing only, should return "Hello world" @@ -71,7 +71,6 @@ fn rocket() -> Rocket { .mount( "/api", routes![ - create_database, sign_up, create_test, login, @@ -83,9 +82,10 @@ fn rocket() -> Rocket { .mount("/api", routes![server, server_info]) // hosts the fileserver .mount("/typing", FileServer::from(relative!("websites/Typing"))) - .mount("/servers", FileServer::from(relative!("websites/Servers"))) - .mount( - "/BitBurner", - FileServer::from(relative!("websites/BitBurner")), - ) + .manage(Database::new().await.unwrap()) + // .mount("/servers", FileServer::from(relative!("websites/Servers"))) + //.mount( + // "/BitBurner", + // FileServer::from(relative!("websites/BitBurner")), + //) } diff --git a/src/typing/sql.rs b/src/typing/sql.rs index f34daee..352047e 100644 --- a/src/typing/sql.rs +++ b/src/typing/sql.rs @@ -10,176 +10,138 @@ //! - create structure for the input of post test // Imports for json handling and rusqlite -use rusqlite::{Connection, Result}; +use sqlx::sqlite::{SqlitePool, SqlitePoolOptions}; use rocket::serde::Serialize; +pub struct Database(SqlitePool); + /// gets a connection to the database and returns it as /// a rusqlite::connection -fn get_connection() -> rusqlite::Connection { - Connection::open("database/database.sqlite") - .expect("Error creating database connection") -} +impl Database { + pub async fn new() -> Result { + let pool = SqlitePoolOptions::new() + .max_connections(2) + .connect("sqlite:/Users/arlo/code/cs_coursework/database/dev/database.sqlite") + .await?; -/// Creates the necessary tables inside the database with -/// correct normalised links between data for later -/// querying -fn new_database() -> Result<()> { - let connection = get_connection(); - - connection.execute( - "CREATE TABLE IF NOT EXISTS users ( - user_id INTEGER PRIMARY KEY, - username TEXT UNIQUE NOT NULL, - password TEXT NOT NULL - )", - () - )?; - - connection.execute( - "CREATE TABLE IF NOT EXISTS tests ( - test_id INTEGER PRIMARY KEY, - test_type TEXT NOT NULL, - test_length INTEGER, - test_time INTEGER, - test_seed INTEGER, - quote_id INTEGER, - wpm INTEGER, - accuracy INTEGER, - user_id INTEGER, - FOREIGN KEY(user_id) REFERENCES users(user_id) - )", - () - )?; - - Ok(()) -} - -/// Api route that creates a database if one -/// does not already exist. -/// Acessible from http://url/api/create_database -#[get("/create_database")] -pub fn create_database() -> String { - let database = new_database(); - match database { - Err(why) => format!("Error: {why}"), - Ok(_) => format!("Sucessfully created the database") + Ok(Self(pool)) } -} -/// takes necessary data about a test and creates -/// a database record with the data -pub fn post_test( - test_type: &str, - test_length: u32, - test_time: u32, - test_seed: i64, - quote_id: i32, - wpm: u8, - accuracy: u8, - user_id: u32 -) -> Result<()> { - let connection = get_connection(); + /// Creates the necessary tables inside the database with + /// correct normalised links between data for later querying + pub async fn new_database(&self) -> Result<(), sqlx::Error> { + sqlx::query!(" + CREATE TABLE IF NOT EXISTS Users ( + user_id INTEGER PRIMARY KEY, + username TEXT UNIQUE NOT NULL, + password TEXT NOT NULL, + secret TEXT NOT NULL + )" + ).execute(&self.0).await?; - connection.execute( - "INSERT INTO tests ( - test_type, - test_length, - test_time, - test_seed, - quote_id, - wpm, - accuracy, - user_id - ) - VALUES( - ?1, - ?2, - ?3, - ?4, - ?5, - ?6, - ?7, - ?8 - ) - ", - ( - test_type, - test_length, - test_time, - test_seed, - quote_id, - wpm, - accuracy, - user_id - ) - )?; + sqlx::query!(" + CREATE TABLE IF NOT EXISTS Tests ( + test_id INTEGER PRIMARY KEY, + test_type TEXT NOT NULL, + test_length INTEGER, + test_time INTEGER, + test_seed INTEGER, + quote_id INTEGER, + wpm INTEGER, + accuracy INTEGER, + user_id INTEGER, + FOREIGN KEY(user_id) REFERENCES users(user_id) + )" + ).execute(&self.0).await?; - Ok(()) -} + Ok(()) + } -/// takes a username and password and creates a database -/// entry for a new user -pub fn create_user( - username: &str, - password: &str -) -> Result<()> { - let connection = get_connection(); + /// takes necessary data about a test and creates + /// a database record with the data + pub async fn create_test(&self, test_type: &str, test_length: u32, test_time: u32, test_seed: i64, quote_id: i32, wpm: u8, accuracy: u8, user_id: u32) -> Result<(), sqlx::Error> { + sqlx::query!(" + INSERT INTO Tests (test_type, test_length, test_time, test_seed, quote_id, wpm, accuracy, user_id) + VALUES(?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)", + test_type, test_length, test_time, test_seed, quote_id, wpm, accuracy, user_id + ).execute(&self.0).await?; - connection.execute( - " - INSERT INTO users ( - username, - password - ) - VALUES ( - ?1, - ?2 - ) - ", - ( - username, - password - ) - )?; + Ok(()) + } - Ok(()) -} + /// takes a username and password and creates a database + /// entry for a new user + pub async fn create_user(&self, username: &str, password: &str) -> Result<(), sqlx::Error> { + sqlx::query!(" + INSERT INTO Users (username, password) + VALUES (?1, ?2)", + username, password + ).execute(&self.0).await?; -/// struct which can be deserialised -/// from json to get the user_id -#[derive(Debug)] -pub struct User { - user_id: u32, -} + Ok(()) + } -/// takes a username and password as inputs and returns the -/// user_id of the user if one exists -pub fn find_user( - username: &str, - password: &str -) -> Result { - let mut user_id: u32 = 0; - let connection = get_connection(); - let mut statement = connection.prepare( - "SELECT user_id - FROM users - WHERE username=:username AND password=:password", - )?; + /// takes a username and password as inputs and returns the + /// user_id and secret of the user if one exists + pub async fn find_user(&self, username: &str, password: &str) -> Result, sqlx::Error> { + let user = sqlx::query!(" + SELECT user_id, secret + FROM Users + WHERE username=:username AND password=:password", + username, password + ).fetch_all(&self.0).await?; - let iter = statement - .query_map( - &[(":username", username), (":password", password)], |row| { - Ok( User { - user_id: row.get(0)? + let user_id = user[0].user_id.unwrap() as u32; + let secret = user[0].secret.clone(); + + Ok(Some((user_id, secret))) + } + + /// returns all the tests that a given user_id has + /// completed from the database + pub async fn get_user_tests(&self, user_id: u32, secret: &str) -> Result, sqlx::Error> { + let tests = sqlx::query!(" + SELECT test_type, test_length, test_time, test_seed, quote_id, wpm, accuracy + FROM tests + INNER JOIN users ON users.user_id = tests.user_id + WHERE users.user_id=:user_id AND users.secret=:secret", + user_id, secret + ).fetch_all(&self.0).await?; + + let user_tests = tests.iter() + .map(|test| Test { + test_type: test.test_type.clone(), + test_length: test.test_length.unwrap() as u32, + test_time: test.test_time.unwrap() as u32, + test_seed: test.test_seed.unwrap(), + quote_id: test.quote_id.unwrap() as i32, + wpm: test.wpm.unwrap() as u8, + accuracy: test.accuracy.unwrap() as u8 }) - } - )?; + .collect(); - for i in iter { - user_id = i.unwrap().user_id; + Ok(user_tests) } + /// returns a vector of leaderboard tests, where each one is the fastest words + /// per minute that a given user has achieved + pub async fn get_leaderboard(&self, _user_id: u32) -> Result, sqlx::Error> { + let tests = sqlx::query!( + "SELECT users.username, tests.wpm + FROM tests + INNER JOIN users ON users.user_id = tests.user_id + GROUP BY users.username + ORDER BY tests.wpm DESC", + ).fetch_all(&self.0).await?; - Ok(user_id) + let leaderboard_tests = tests.iter() + .map(|test| LeaderBoardTest { + username: test.username.clone(), + wpm: test.wpm.unwrap() as u8 + }) + .collect(); + + Ok(leaderboard_tests) + } } /// struct representing data that needs to be sent @@ -196,39 +158,6 @@ pub struct Test { accuracy: u8, } -/// returns all the tests that a given user_id has -/// completed from the database -pub fn get_user_tests( - user_id: u32 -) -> Result> { - let connection = get_connection(); - let mut statement = connection.prepare( - "SELECT test_type, test_length, test_time, test_seed, quote_id, wpm, accuracy - FROM tests - WHERE user_id=:user_id", - )?; - - let test_iter = statement - .query_map(&[(":user_id", &user_id.to_string())], |row| { - Ok( Test { - test_type: row.get(0)?, - test_length: row.get(1)?, - test_time: row.get(2)?, - test_seed: row.get(3)?, - quote_id: row.get(4)?, - wpm: row.get(5)?, - accuracy: row.get(6)? - }) - })?; - - let mut tests: Vec = vec![]; - for test in test_iter { - tests.push(test.unwrap()); - } - - Ok(tests) -} - /// struct that represents all the data that gets sent to the user /// when they make a leaderboard request #[derive(Serialize)] @@ -238,32 +167,3 @@ pub struct LeaderBoardTest { wpm: u8, } -/// returns a vector of leaderboard tests, where each one is the fastest words -/// per minute that a given user has achieved -pub fn get_leaderboard( - _user_id: u32 -) -> Result>{ - let connection = get_connection(); - let mut statement = connection.prepare( - "SELECT users.username, MAX(tests.wpm) - FROM tests - INNER JOIN users ON users.user_id = tests.user_id - GROUP BY users.username - ORDER BY tests.wpm DESC", - )?; - - let test_iter = statement - .query_map((), |row| { - Ok( LeaderBoardTest { - username: row.get(0)?, - wpm: row.get(1)? - }) - })?; - - let mut tests: Vec = vec![]; - for test in test_iter { - tests.push(test.unwrap()); - } - - Ok(tests) -} \ No newline at end of file diff --git a/src/typing/user.rs b/src/typing/user.rs index 9c7a2bf..c1d4d1d 100644 --- a/src/typing/user.rs +++ b/src/typing/user.rs @@ -1,15 +1,9 @@ -use rocket::serde::{ - Deserialize, - json::Json +use rocket::{ + serde::{Deserialize, json::Json, Serialize}, + State }; -use crate::typing::sql::{ - get_user_tests, - find_user, - create_user, - - Test, -}; +use crate::typing::sql::{ Database, Test }; /// Struct representing the user #[derive(Deserialize)] @@ -23,28 +17,56 @@ pub struct User<'r> { /// and then creates the user in the database /// Acessible from http://url/api/create_user #[post("/create_user", data = "")] -pub fn sign_up(user: Json>) { - create_user( - user.username, - &sha256::digest(user.password), - ).expect("Error: Couldn't create new user"); +pub async fn sign_up(user: Json>, database: &State) { + match database.create_user( user.username, &sha256::digest(user.password) ).await { + Err(why) => { + println!("A database error occured during signup, {}", why); + } + Ok(()) => { + println!("Succesfully Signed up user {}", user.username); + } + } } /// Gets the users tests from the database and returns it as a /// json array. /// Accessible from http://url/api/get_user_tests -#[get("/get_tests/")] -pub fn get_tests(user_id: u32) -> Json> { - let tests: Vec = get_user_tests(user_id).expect("error finding user_id"); - Json(tests) +#[get("/get_tests//")] +pub async fn get_tests(user_id: u32, secret: String, database: &State) -> Option>> { + match database.get_user_tests(user_id, &secret).await { + Err(why) => { + println!("A database error occured during getting_tests, {why}"); + None + } + Ok(tests) => { + println!("Succesfully Found Tests for User {user_id}"); + Some(Json(tests)) + } + } } /// takes the users login information and returns the users user id /// which can be used to identify their tests etc. /// Accessible from http://url/api/login #[get("/login//")] -pub fn login(username: &str, password: &str) -> String { - let user_id = find_user(username, &sha256::digest(password)).expect("error finding user_id"); - user_id.to_string() +pub async fn login(username: &str, password: &str, database: &State) -> Option> { + match database.find_user(username, &sha256::digest(password)).await { + Err(why) => { + println!("A database error occured during login for {username}, {why}"); + None + } + Ok(user) => { + match user { + None => None, + Some(user) => { Some(Json(LoginResponse { user_id: user.0, secret: user.1 })) } + } + } + } } +#[derive(Serialize)] +#[serde(crate = "rocket::serde")] +struct LoginResponse { + user_id: u32, + secret: String, +}