new catchers for 404 errors

This commit is contained in:
Arlo Filley 2024-03-21 17:47:17 +00:00
parent 4a57128a27
commit 45e7887a7e
12 changed files with 377 additions and 155 deletions

3
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,3 @@
{
"rust-analyzer.showUnlinkedFileNotification": false
}

View File

@ -0,0 +1,26 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="github-markdown.css">
</head>
<body class="markdown-body">
<style>
.markdown-body {
box-sizing: border-box;
min-width: 200px;
max-width: 980px;
margin: 0 auto;
padding: 45px;
}
@media (max-width: 767px) {
.markdown-body {
padding: 15px;
}
}
</style>
<h1>The content you are looking for does not appear to be here...</h1>
<a href="/api/documentation/"><button>main page</button></a>
</body>
</html>

View File

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Not Found</title>
</head>
<body >
The content you are looking for does not appear to be here...
<a href="/typing/"><button>main page</button></a>
</body>
</html>

View File

@ -1,8 +1,5 @@
use rocket::{ serde::json::Json, State }; use crate::api::sql::{Database, LeaderBoardTest};
use crate::typing::sql::{ use rocket::{serde::json::Json, State};
Database,
LeaderBoardTest
};
type LeaderBoardTests = Vec<LeaderBoardTest>; type LeaderBoardTests = Vec<LeaderBoardTest>;
@ -10,15 +7,14 @@ type LeaderBoardTests = Vec<LeaderBoardTest>;
/// a json array /// a json array
/// Acessible from http://url/api/leaderboard /// Acessible from http://url/api/leaderboard
#[get("/leaderboard")] #[get("/leaderboard")]
pub async fn leaderboard( database: &State<Database> ) -> Option<Json<LeaderBoardTests>> { pub async fn leaderboard(database: &State<Database>) -> Option<Json<LeaderBoardTests>> {
let leaderboard = match database.get_leaderboard(0).await { let leaderboard = match database.get_leaderboard(0).await {
Err(why) => { Err(why) => {
println!("Error getting leaderboard, {why}"); println!("Error getting leaderboard, {why}");
return None return None;
} }
Ok(leaderboard) => { leaderboard } Ok(leaderboard) => leaderboard,
}; };
Some(Json(leaderboard)) Some(Json(leaderboard))
} }

View File

@ -1,4 +1,4 @@
pub mod sql;
pub mod user;
pub mod leaderboard; pub mod leaderboard;
pub mod sql;
pub mod test; pub mod test;
pub mod user;

View File

@ -10,10 +10,10 @@
//! - 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 sqlx::sqlite::{ SqlitePool, SqlitePoolOptions }; use rocket::serde::{json::Json, Serialize};
use rocket::serde::{ Serialize, json::Json }; use sqlx::sqlite::{SqlitePool, SqlitePoolOptions};
use crate::typing::test::PostTest; use crate::api::test::PostTest;
/// Contains the database connection pool /// Contains the database connection pool
pub struct Database(SqlitePool); pub struct Database(SqlitePool);
@ -24,7 +24,7 @@ impl Database {
pub async fn new() -> Result<Self, sqlx::Error> { pub async fn new() -> Result<Self, sqlx::Error> {
let pool = SqlitePoolOptions::new() let pool = SqlitePoolOptions::new()
.max_connections(2) .max_connections(2)
.connect("sqlite:/Users/arlo/code/cs_coursework/database/dev/database.sqlite") .connect("sqlite:/Users/arlo/Code/Projects/cs_coursework/database/database.sqlite")
.await?; .await?;
Ok(Self(pool)) Ok(Self(pool))
@ -33,16 +33,20 @@ impl Database {
/// Creates the necessary tables inside the database with /// Creates the necessary tables inside the database with
/// correct normalised links between data for later querying /// correct normalised links between data for later querying
pub async fn _new_database(&self) -> Result<(), sqlx::Error> { pub async fn _new_database(&self) -> Result<(), sqlx::Error> {
sqlx::query!(" sqlx::query!(
"
CREATE TABLE IF NOT EXISTS Users ( 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 secret TEXT NOT NULL
)" )"
).execute(&self.0).await?; )
.execute(&self.0)
.await?;
sqlx::query!(" 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,
@ -55,7 +59,9 @@ impl Database {
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?; )
.execute(&self.0)
.await?;
Ok(()) Ok(())
} }
@ -64,15 +70,18 @@ impl Database {
/// a database record with the data /// a database record with the data
pub async fn create_test(&self, test: Json<PostTest<'_>>) -> Result<(), sqlx::Error> { pub async fn create_test(&self, test: Json<PostTest<'_>>) -> Result<(), sqlx::Error> {
// Test to see whether the secret is correct // Test to see whether the secret is correct
let user = sqlx::query!(" let user = sqlx::query!(
"
Select secret Select secret
From Users From Users
Where user_id=?", Where user_id=?",
test.user_id test.user_id
).fetch_one(&self.0).await?; )
.fetch_one(&self.0)
.await?;
if user.secret != test.secret { if user.secret != test.secret {
return Err(sqlx::Error::RowNotFound) return Err(sqlx::Error::RowNotFound);
} }
sqlx::query!(" sqlx::query!("
@ -86,25 +95,43 @@ impl Database {
/// 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 async fn create_user(&self, username: &str, password: &str, secret: &str) -> Result<(), sqlx::Error> { pub async fn create_user(
sqlx::query!(" &self,
username: &str,
password: &str,
secret: &str,
) -> Result<(), sqlx::Error> {
sqlx::query!(
"
INSERT INTO Users (username, password, secret) INSERT INTO Users (username, password, secret)
VALUES (?1, ?2, ?3)", VALUES (?1, ?2, ?3)",
username, password, secret username,
).execute(&self.0).await?; password,
secret
)
.execute(&self.0)
.await?;
Ok(()) Ok(())
} }
/// takes a username and password as inputs and returns the /// takes a username and password as inputs and returns the
/// user_id and secret of the user if one exists /// user_id and secret of the user if one exists
pub async fn find_user(&self, username: &str, password: &str) -> Result<Option<(u32, String)>, sqlx::Error> { pub async fn find_user(
let user = sqlx::query!(" &self,
username: &str,
password: &str,
) -> Result<Option<(u32, String)>, sqlx::Error> {
let user = sqlx::query!(
"
SELECT user_id, secret SELECT user_id, secret
FROM Users FROM Users
WHERE username=? AND password=?", WHERE username=? AND password=?",
username, password username,
).fetch_one(&self.0).await?; password
)
.fetch_one(&self.0)
.await?;
let user_id = user.user_id.unwrap() as u32; let user_id = user.user_id.unwrap() as u32;
let secret = user.secret.clone(); let secret = user.secret.clone();
@ -114,18 +141,27 @@ impl Database {
/// returns all the tests that a given user_id has /// returns all the tests that a given user_id has
/// completed from the database /// completed from the database
pub async fn get_user_tests(&self, user_id: u32, secret: &str) -> Result<Vec<Test>, sqlx::Error> { pub async fn get_user_tests(
let tests = sqlx::query!(" &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 SELECT test_type, test_length, test_time, test_seed, quote_id, wpm, accuracy
FROM tests FROM tests
INNER JOIN users ON users.user_id = tests.user_id INNER JOIN users ON users.user_id = tests.user_id
WHERE users.user_id=? AND users.secret=?", WHERE users.user_id=? AND users.secret=?",
user_id, secret user_id,
).fetch_all(&self.0).await?; secret
)
.fetch_all(&self.0)
.await?;
println!("{}", tests.len()); println!("{}", tests.len());
let user_tests = tests.iter() let user_tests = tests
.iter()
.map(|test| Test { .map(|test| Test {
test_type: test.test_type.clone(), test_type: test.test_type.clone(),
test_length: test.test_length.unwrap() as u32, test_length: test.test_length.unwrap() as u32,
@ -133,32 +169,83 @@ impl Database {
test_seed: test.test_seed.unwrap(), test_seed: test.test_seed.unwrap(),
quote_id: test.quote_id.unwrap() as i32, quote_id: test.quote_id.unwrap() as i32,
wpm: test.wpm.unwrap() as u8, wpm: test.wpm.unwrap() as u8,
accuracy: test.accuracy.unwrap() as u8 accuracy: test.accuracy.unwrap() as u8,
}) })
.collect(); .collect();
Ok(user_tests) Ok(user_tests)
} }
/// returns a vector of leaderboard tests, where each one is the fastest words /// returns a vector of leaderboard tests, where each one is the fastest words
/// per minute that a given user has achieved /// per minute that a given user has achieved
pub async fn get_leaderboard(&self, _user_id: u32) -> Result<Vec<LeaderBoardTest>, sqlx::Error> { pub async fn get_leaderboard(
&self,
_user_id: u32,
) -> Result<Vec<LeaderBoardTest>, sqlx::Error> {
let tests = sqlx::query!( let tests = sqlx::query!(
"SELECT users.username, tests.wpm "SELECT users.username, tests.wpm
FROM tests FROM tests
INNER JOIN users ON users.user_id = tests.user_id INNER JOIN users ON users.user_id = tests.user_id
GROUP BY users.username GROUP BY users.username
ORDER BY tests.wpm DESC", ORDER BY tests.wpm DESC",
).fetch_all(&self.0).await?; )
.fetch_all(&self.0)
.await?;
let leaderboard_tests = tests.iter() let leaderboard_tests = tests
.iter()
.map(|test| LeaderBoardTest { .map(|test| LeaderBoardTest {
username: test.username.clone(), username: test.username.clone(),
wpm: test.wpm.unwrap() as u8 wpm: test.wpm.unwrap() as u8,
}) })
.collect(); .collect();
Ok(leaderboard_tests) Ok(leaderboard_tests)
} }
/// Authenticates a user based on their user ID and secret.
///
/// # Arguments
///
/// * `user_id` - The ID of the user to authenticate.
/// * `secret` - The secret associated with the user.
///
/// # Returns
///
/// Returns a `Result` indicating whether the authentication was successful or not.
/// - `Ok(true)` if authentication is successful.
/// - `Ok(false)` if authentication fails.
/// - `Err` if there's an error accessing the database.
///
/// # Examples
///
/// ```rust
/// use crate::sql::Database;
///
/// #[tokio::main]
/// async fn main() {
/// let db = Database::new().await.expect("Failed to create database connection");
///
/// // Authenticate user with user ID 123 and secret "example_secret"
/// let authenticated = db.authenticate_user(123, "example_secret").await;
/// assert_eq!(authenticated, Ok(true));
/// }
/// ```
pub async fn authenticate_user(&self, user_id: u32, secret: &str) -> Result<bool, sqlx::Error> {
// Test to see whether the secret is correct
let user = sqlx::query!(
"
Select secret
From Users
Where user_id=?",
user_id
)
.fetch_one(&self.0)
.await?;
// Compare the fetched secret with the provided one
Ok(user.secret == secret)
}
} }
/// struct representing data that needs to be sent /// struct representing data that needs to be sent

View File

@ -1,16 +1,10 @@
use crate::typing::sql::Database; use crate::api::sql::Database;
use rand::{rngs::ThreadRng, Rng};
use rocket::{ use rocket::{
serde::{ Deserialize, json::Json }, serde::{json::Json, Deserialize},
State State,
};
use rand::{
Rng,
rngs::ThreadRng
};
use std::{
fs,
vec,
}; };
use std::{fs, vec};
/// the datascructure that the webserver will recieve /// the datascructure that the webserver will recieve
/// when a post is made to the http://url/api/post_test route /// when a post is made to the http://url/api/post_test route
@ -25,7 +19,7 @@ pub struct PostTest<'r> {
pub wpm: u8, pub wpm: u8,
pub accuracy: u8, pub accuracy: u8,
pub user_id: u32, pub user_id: u32,
pub secret: &'r str pub secret: &'r str,
} }
/// Api Route that accepts test data and posts it to the database /// Api Route that accepts test data and posts it to the database
@ -34,8 +28,12 @@ pub struct PostTest<'r> {
pub async fn create_test(test: Json<PostTest<'_>>, database: &State<Database>) { pub async fn create_test(test: Json<PostTest<'_>>, database: &State<Database>) {
let user_id = test.user_id; let user_id = test.user_id;
match database.create_test(test).await { match database.create_test(test).await {
Err(why) => { println!("A database error occured creating a test, {why}"); } Err(why) => {
Ok(()) => { println!("Successfully created test for {user_id}"); } println!("A database error occured creating a test, {why}");
}
Ok(()) => {
println!("Successfully created test for {user_id}");
}
} }
} }

148
src/api/user.rs Normal file
View File

@ -0,0 +1,148 @@
use rocket::{
http::Status,
serde::{json::Json, Deserialize, Serialize},
State,
};
use rand::{distributions::Alphanumeric, Rng};
use crate::api::sql::{Database, Test};
/// Struct representing the user
#[derive(Deserialize)]
#[serde(crate = "rocket::serde")]
pub struct User<'r> {
username: &'r str,
password: &'r str,
}
/// Route takes data about the user as a struct
/// and then creates the user in the database
/// Acessible from http://url/api/create_user
#[post("/create_user", data = "<user>")]
pub async fn sign_up(user: Json<User<'_>>, database: &State<Database>) {
let secret: String = rand::thread_rng()
.sample_iter(&Alphanumeric)
.take(50)
.map(char::from)
.collect();
match database
.create_user(user.username, &sha256::digest(user.password), &secret)
.await
{
Err(why) => {
println!("A database error occured during signup, {why}");
}
Ok(()) => {
println!("Succesfully Signed up User: {}", user.username);
}
}
}
/// Retrieves tests associated with a specific user from the database and returns them as a JSON array.
///
/// # Endpoint
///
/// GET /api/get_tests/<user_id>/<secret>
///
/// # Path Parameters
///
/// - `user_id`: User ID of the user whose tests need to be retrieved.
/// - `secret`: Secret key for authentication.
///
/// # Returns
///
/// Returns a JSON array containing the user's tests if the user is authenticated and the tests are found.
///
/// If the user authentication fails, returns a `401 Unauthorized` status.
///
/// If the tests are not found or any database-related error occurs, returns a `404 Not Found` status.
///
/// # Example Request
///
/// ```bash
/// curl -X GET "https://example.com/api/get_tests/123/your_secret_key_here"
/// ```
///
/// # Example Response
///
/// ```json
/// [
/// {
/// "test_type": "typing",
/// "test_length": 100,
/// "test_time": 300,
/// "test_seed": 987654321,
/// "quote_id": 123,
/// "wpm": 65,
/// "accuracy": 98
/// },
/// {
/// "test_type": "multiple_choice",
/// "test_length": 50,
/// "test_time": 150,
/// "test_seed": 123456789,
/// "quote_id": null,
/// "wpm": null,
/// "accuracy": 85
/// }
/// ]
/// ```
#[get("/get_tests/<user_id>/<secret>")]
pub async fn get_tests(user_id: u32, secret: &str, database: &State<Database>) -> Result<Json<Vec<Test>>, Status> {
match database.authenticate_user(user_id, &secret).await {
Err(_) => return Err(Status::InternalServerError),
Ok(authenticated) => {
if !authenticated {
return Err(Status::Unauthorized);
}
}
}
match database.get_user_tests(user_id, &secret).await {
Err(why) => {
println!("A database error occured during getting_tests, {why}");
Err(Status::NotFound)
}
Ok(tests) => {
println!("Succesfully Found Tests for User {user_id}");
Ok(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/<username>/<password>")]
pub async fn login(
username: &str,
password: &str,
database: &State<Database>,
) -> Json<Option<LoginResponse>> {
match database
.find_user(username, &sha256::digest(password))
.await
{
Err(why) => {
println!("A database error occured during login for {username}, {why}");
Json(None)
}
Ok(user) => match user {
None => Json(None),
Some(user) => Json(Some(LoginResponse {
user_id: user.0,
secret: user.1,
})),
},
}
}
#[derive(Serialize)]
#[serde(crate = "rocket::serde")]
pub struct LoginResponse {
user_id: u32,
secret: String,
}

1
src/catchers/mod.rs Normal file
View File

@ -0,0 +1 @@
pub mod not_found;

16
src/catchers/not_found.rs Normal file
View File

@ -0,0 +1,16 @@
use rocket::{response::content::RawHtml, Request};
#[catch(404)]
pub fn api_not_found(req: &Request) -> String {
format!("Sorry, '{}' couldn't be found.", req.uri())
}
#[catch(404)]
pub fn frontend_not_found<'a>(_req: &Request) -> RawHtml<&'a str> {
RawHtml(include_str!("../../public/Error/not_found.html"))
}
#[catch(404)]
pub fn documentation_not_found<'a>(_req: &Request) -> RawHtml<&'a str> {
RawHtml(include_str!("../../documentation/not_found.html"))
}

View File

@ -40,12 +40,17 @@ use rocket::{
Build, Rocket, Build, Rocket,
}; };
mod typing; mod api;
mod catchers;
use crate::typing::leaderboard::leaderboard; use crate::api::leaderboard::leaderboard;
use crate::typing::sql::Database; use crate::api::sql::Database;
use crate::typing::test::{create_test, new_test}; use crate::api::test::{create_test, new_test};
use crate::typing::user::{get_tests, login, sign_up}; use crate::api::user::{get_tests, login, sign_up};
use catchers::not_found::api_not_found;
use catchers::not_found::frontend_not_found;
use catchers::not_found::documentation_not_found;
// Imports for sql, see sql.rs for more information // Imports for sql, see sql.rs for more information
@ -66,6 +71,8 @@ async fn rocket() -> Rocket<Build> {
.mount("/test", routes![test]) .mount("/test", routes![test])
// hosts the api routes necessary for the website // hosts the api routes necessary for the website
// to interact with the database // to interact with the database
.mount("/api/documentation", FileServer::from(relative!("documentation")))
.register("/api/documentation", catchers![documentation_not_found])
.mount( .mount(
"/api", "/api",
routes![ routes![
@ -77,7 +84,11 @@ async fn rocket() -> Rocket<Build> {
new_test, new_test,
], ],
) )
.register("/api", catchers![api_not_found])
// hosts the fileserver // hosts the fileserver
.mount("/typing", FileServer::from(relative!("websites/Typing"))) .mount("/typing", FileServer::from(relative!("public")))
.register("/typing", catchers![frontend_not_found])
.manage(Database::new().await.unwrap()) .manage(Database::new().await.unwrap())
} }

View File

@ -1,76 +0,0 @@
use rocket::{
serde::{Deserialize, json::Json, Serialize},
State
};
use rand::{ distributions::Alphanumeric, Rng };
use crate::typing::sql::{ Database, Test };
/// Struct representing the user
#[derive(Deserialize)]
#[serde(crate = "rocket::serde")]
pub struct User<'r> {
username: &'r str,
password: &'r str
}
/// Route takes data about the user as a struct
/// and then creates the user in the database
/// Acessible from http://url/api/create_user
#[post("/create_user", data = "<user>")]
pub async fn sign_up(user: Json<User<'_>>, database: &State<Database>) {
let secret: String = rand::thread_rng()
.sample_iter(&Alphanumeric)
.take(50)
.map(char::from)
.collect();
match database.create_user( user.username, &sha256::digest(user.password), &secret ).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/<user_id>/<secret>")]
pub async fn get_tests(user_id: u32, secret: String, database: &State<Database>) -> Option<Json<Vec<Test>>> {
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/<username>/<password>")]
pub async fn login(username: &str, password: &str, database: &State<Database>) -> Json<Option<LoginResponse>> {
match database.find_user(username, &sha256::digest(password)).await {
Err(why) => {
println!("A database error occured during login for {username}, {why}");
Json(None)
}
Ok(user) => {
match user {
None => Json(None),
Some(user) => { Json(Some(LoginResponse { user_id: user.0, secret: user.1 })) }
}
}
}
}
#[derive(Serialize)]
#[serde(crate = "rocket::serde")]
pub struct LoginResponse {
user_id: u32,
secret: String,
}