migrated database to sqlite

This commit is contained in:
Arlo Filley 2023-09-04 16:53:09 +01:00
parent 2ae7f236ce
commit b3ed15811e
8 changed files with 172 additions and 259 deletions

12
.gitignore vendored
View File

@ -1,13 +1,5 @@
/target /target
Cargo.lock Cargo.lock
/database/database.sqlite
/NEA_Screenshots
.DS_Store .DS_Store
.gitignore .env
/.database
start.sh
TODO.md
/server
/servers_old
Rocket.toml
/.hidden

View File

@ -4,9 +4,9 @@ version = "0.1.0"
edition = "2021" edition = "2021"
[dependencies] [dependencies]
rusqlite = "0.28.0"
rand = "0.8.5" rand = "0.8.5"
sha256 = "1.1.1" sha256 = "1.1.1"
sqlx = { version = "0.7.1", features = ["macros", "sqlite", "runtime-tokio"] }
[dependencies.rocket] [dependencies.rocket]
version = "0.5.0-rc.2" version = "0.5.0-rc.2"

View File

@ -1,2 +0,0 @@
### Backend
- [ ] hello there

BIN
database/dev/database.sqlite Executable file

Binary file not shown.

1
done.md Normal file
View File

@ -0,0 +1 @@
- Migrated to sqlx

View File

@ -45,7 +45,7 @@ mod typing;
use crate::servers::server::{server, server_info}; use crate::servers::server::{server, server_info};
use crate::typing::leaderboard::leaderboard; 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::test::{create_test, new_test};
use crate::typing::user::{get_tests, login, sign_up}; 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. /// Test api route that returns hello world.
/// Acessible from http://url/test /// Acessible from http://url/test
#[get("/")] #[get("/")]
fn test() -> String { fn test() -> &'static str {
String::from("Hello World!") "Hello World! I'm A rocket Webserver"
} }
/// The main function which builds and launches the /// The main function which builds and launches the
/// webserver with all appropriate routes and fileservers /// webserver with all appropriate routes and fileservers
#[launch] #[launch]
fn rocket() -> Rocket<Build> { async fn rocket() -> Rocket<Build> {
rocket::build() rocket::build()
.attach(CORS) .attach(CORS)
// testing only, should return "Hello world" // testing only, should return "Hello world"
@ -71,7 +71,6 @@ fn rocket() -> Rocket<Build> {
.mount( .mount(
"/api", "/api",
routes![ routes![
create_database,
sign_up, sign_up,
create_test, create_test,
login, login,
@ -83,9 +82,10 @@ fn rocket() -> Rocket<Build> {
.mount("/api", routes![server, server_info]) .mount("/api", routes![server, server_info])
// hosts the fileserver // hosts the fileserver
.mount("/typing", FileServer::from(relative!("websites/Typing"))) .mount("/typing", FileServer::from(relative!("websites/Typing")))
.mount("/servers", FileServer::from(relative!("websites/Servers"))) .manage(Database::new().await.unwrap())
.mount( // .mount("/servers", FileServer::from(relative!("websites/Servers")))
"/BitBurner", //.mount(
FileServer::from(relative!("websites/BitBurner")), // "/BitBurner",
) // FileServer::from(relative!("websites/BitBurner")),
//)
} }

View File

@ -10,33 +10,37 @@
//! - create structure for the input of post test //! - create structure for the input of post test
// Imports for json handling and rusqlite // Imports for json handling and rusqlite
use rusqlite::{Connection, Result}; use sqlx::sqlite::{SqlitePool, SqlitePoolOptions};
use rocket::serde::Serialize; use rocket::serde::Serialize;
pub struct Database(SqlitePool);
/// gets a connection to the database and returns it as /// gets a connection to the database and returns it as
/// a rusqlite::connection /// a rusqlite::connection
fn get_connection() -> rusqlite::Connection { impl Database {
Connection::open("database/database.sqlite") pub async fn new() -> Result<Self, sqlx::Error> {
.expect("Error creating database connection") let pool = SqlitePoolOptions::new()
.max_connections(2)
.connect("sqlite:/Users/arlo/code/cs_coursework/database/dev/database.sqlite")
.await?;
Ok(Self(pool))
} }
/// Creates the necessary tables inside the database with /// Creates the necessary tables inside the database with
/// correct normalised links between data for later /// correct normalised links between data for later querying
/// querying pub async fn new_database(&self) -> Result<(), sqlx::Error> {
fn new_database() -> Result<()> { sqlx::query!("
let connection = get_connection(); CREATE TABLE IF NOT EXISTS Users (
connection.execute(
"CREATE TABLE IF NOT EXISTS users (
user_id INTEGER PRIMARY KEY, user_id INTEGER PRIMARY KEY,
username TEXT UNIQUE NOT NULL, username TEXT UNIQUE NOT NULL,
password TEXT NOT NULL password TEXT NOT NULL,
)", secret TEXT NOT NULL
() )"
)?; ).execute(&self.0).await?;
connection.execute( sqlx::query!("
"CREATE TABLE IF NOT EXISTS tests ( CREATE TABLE IF NOT EXISTS Tests (
test_id INTEGER PRIMARY KEY, test_id INTEGER PRIMARY KEY,
test_type TEXT NOT NULL, test_type TEXT NOT NULL,
test_length INTEGER, test_length INTEGER,
@ -47,139 +51,97 @@ fn new_database() -> Result<()> {
accuracy INTEGER, accuracy INTEGER,
user_id INTEGER, user_id INTEGER,
FOREIGN KEY(user_id) REFERENCES users(user_id) FOREIGN KEY(user_id) REFERENCES users(user_id)
)", )"
() ).execute(&self.0).await?;
)?;
Ok(()) 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")
}
}
/// takes necessary data about a test and creates /// takes necessary data about a test and creates
/// a database record with the data /// a database record with the data
pub fn post_test( 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> {
test_type: &str, sqlx::query!("
test_length: u32, INSERT INTO Tests (test_type, test_length, test_time, test_seed, quote_id, wpm, accuracy, user_id)
test_time: u32, VALUES(?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)",
test_seed: i64, test_type, test_length, test_time, test_seed, quote_id, wpm, accuracy, user_id
quote_id: i32, ).execute(&self.0).await?;
wpm: u8,
accuracy: u8,
user_id: u32
) -> Result<()> {
let connection = get_connection();
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
)
)?;
Ok(()) Ok(())
} }
/// takes a username and password and creates a database /// takes a username and password and creates a database
/// entry for a new user /// entry for a new user
pub fn create_user( pub async fn create_user(&self, username: &str, password: &str) -> Result<(), sqlx::Error> {
username: &str, sqlx::query!("
password: &str INSERT INTO Users (username, password)
) -> Result<()> { VALUES (?1, ?2)",
let connection = get_connection(); username, password
).execute(&self.0).await?;
connection.execute(
"
INSERT INTO users (
username,
password
)
VALUES (
?1,
?2
)
",
(
username,
password
)
)?;
Ok(()) Ok(())
} }
/// struct which can be deserialised
/// from json to get the user_id
#[derive(Debug)]
pub struct User {
user_id: u32,
}
/// takes a username and password as inputs and returns the /// takes a username and password as inputs and returns the
/// user_id of the user if one exists /// user_id and secret of the user if one exists
pub fn find_user( pub async fn find_user(&self, username: &str, password: &str) -> Result<Option<(u32, String)>, sqlx::Error> {
username: &str, let user = sqlx::query!("
password: &str SELECT user_id, secret
) -> Result<u32> { FROM Users
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", WHERE username=:username AND password=:password",
)?; username, password
).fetch_all(&self.0).await?;
let iter = statement let user_id = user[0].user_id.unwrap() as u32;
.query_map( let secret = user[0].secret.clone();
&[(":username", username), (":password", password)], |row| {
Ok( User { Ok(Some((user_id, secret)))
user_id: row.get(0)? }
/// 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<Vec<Test>, 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 { Ok(user_tests)
user_id = i.unwrap().user_id;
} }
/// 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<Vec<LeaderBoardTest>, 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 /// struct representing data that needs to be sent
@ -196,39 +158,6 @@ pub struct Test {
accuracy: u8, 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<Vec<Test>> {
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<Test> = vec![];
for test in test_iter {
tests.push(test.unwrap());
}
Ok(tests)
}
/// struct that represents all the data that gets sent to the user /// struct that represents all the data that gets sent to the user
/// when they make a leaderboard request /// when they make a leaderboard request
#[derive(Serialize)] #[derive(Serialize)]
@ -238,32 +167,3 @@ pub struct LeaderBoardTest {
wpm: u8, 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<Vec<LeaderBoardTest>>{
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<LeaderBoardTest> = vec![];
for test in test_iter {
tests.push(test.unwrap());
}
Ok(tests)
}

View File

@ -1,15 +1,9 @@
use rocket::serde::{ use rocket::{
Deserialize, serde::{Deserialize, json::Json, Serialize},
json::Json State
}; };
use crate::typing::sql::{ use crate::typing::sql::{ Database, Test };
get_user_tests,
find_user,
create_user,
Test,
};
/// Struct representing the user /// Struct representing the user
#[derive(Deserialize)] #[derive(Deserialize)]
@ -23,28 +17,56 @@ pub struct User<'r> {
/// and then creates the user in the database /// and then creates the user in the database
/// Acessible from http://url/api/create_user /// Acessible from http://url/api/create_user
#[post("/create_user", data = "<user>")] #[post("/create_user", data = "<user>")]
pub fn sign_up(user: Json<User<'_>>) { pub async fn sign_up(user: Json<User<'_>>, database: &State<Database>) {
create_user( match database.create_user( user.username, &sha256::digest(user.password) ).await {
user.username, Err(why) => {
&sha256::digest(user.password), println!("A database error occured during signup, {}", why);
).expect("Error: Couldn't create new user"); }
Ok(()) => {
println!("Succesfully Signed up user {}", user.username);
}
}
} }
/// Gets the users tests from the database and returns it as a /// Gets the users tests from the database and returns it as a
/// json array. /// json array.
/// Accessible from http://url/api/get_user_tests /// Accessible from http://url/api/get_user_tests
#[get("/get_tests/<user_id>")] #[get("/get_tests/<user_id>/<secret>")]
pub fn get_tests(user_id: u32) -> Json<Vec<Test>> { pub async fn get_tests(user_id: u32, secret: String, database: &State<Database>) -> Option<Json<Vec<Test>>> {
let tests: Vec<Test> = get_user_tests(user_id).expect("error finding user_id"); match database.get_user_tests(user_id, &secret).await {
Json(tests) 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 /// takes the users login information and returns the users user id
/// which can be used to identify their tests etc. /// which can be used to identify their tests etc.
/// Accessible from http://url/api/login /// Accessible from http://url/api/login
#[get("/login/<username>/<password>")] #[get("/login/<username>/<password>")]
pub fn login(username: &str, password: &str) -> String { pub async fn login(username: &str, password: &str, database: &State<Database>) -> Option<Json<LoginResponse>> {
let user_id = find_user(username, &sha256::digest(password)).expect("error finding user_id"); match database.find_user(username, &sha256::digest(password)).await {
user_id.to_string() 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,
}