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::typing::sql::{
Database,
LeaderBoardTest
};
use crate::api::sql::{Database, LeaderBoardTest};
use rocket::{serde::json::Json, State};
type LeaderBoardTests = Vec<LeaderBoardTest>;
@ -10,15 +7,14 @@ type LeaderBoardTests = Vec<LeaderBoardTest>;
/// a json array
/// Acessible from http://url/api/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 {
Err(why) => {
println!("Error getting leaderboard, {why}");
return None
return None;
}
Ok(leaderboard) => { leaderboard }
Ok(leaderboard) => leaderboard,
};
Some(Json(leaderboard))
}

View File

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

View File

@ -10,10 +10,10 @@
//! - create structure for the input of post test
// Imports for json handling and rusqlite
use sqlx::sqlite::{ SqlitePool, SqlitePoolOptions };
use rocket::serde::{ Serialize, json::Json };
use rocket::serde::{json::Json, Serialize};
use sqlx::sqlite::{SqlitePool, SqlitePoolOptions};
use crate::typing::test::PostTest;
use crate::api::test::PostTest;
/// Contains the database connection pool
pub struct Database(SqlitePool);
@ -24,7 +24,7 @@ impl Database {
pub async fn new() -> Result<Self, sqlx::Error> {
let pool = SqlitePoolOptions::new()
.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?;
Ok(Self(pool))
@ -33,16 +33,20 @@ impl Database {
/// 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!("
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?;
)
.execute(&self.0)
.await?;
sqlx::query!("
sqlx::query!(
"
CREATE TABLE IF NOT EXISTS Tests (
test_id INTEGER PRIMARY KEY,
test_type TEXT NOT NULL,
@ -55,7 +59,9 @@ impl Database {
user_id INTEGER,
FOREIGN KEY(user_id) REFERENCES users(user_id)
)"
).execute(&self.0).await?;
)
.execute(&self.0)
.await?;
Ok(())
}
@ -64,15 +70,18 @@ impl Database {
/// a database record with the data
pub async fn create_test(&self, test: Json<PostTest<'_>>) -> Result<(), sqlx::Error> {
// Test to see whether the secret is correct
let user = sqlx::query!("
let user = sqlx::query!(
"
Select secret
From Users
Where user_id=?",
test.user_id
).fetch_one(&self.0).await?;
)
.fetch_one(&self.0)
.await?;
if user.secret != test.secret {
return Err(sqlx::Error::RowNotFound)
return Err(sqlx::Error::RowNotFound);
}
sqlx::query!("
@ -86,25 +95,43 @@ impl Database {
/// takes a username and password and creates a database
/// entry for a new user
pub async fn create_user(&self, username: &str, password: &str, secret: &str) -> Result<(), sqlx::Error> {
sqlx::query!("
pub async fn create_user(
&self,
username: &str,
password: &str,
secret: &str,
) -> Result<(), sqlx::Error> {
sqlx::query!(
"
INSERT INTO Users (username, password, secret)
VALUES (?1, ?2, ?3)",
username, password, secret
).execute(&self.0).await?;
username,
password,
secret
)
.execute(&self.0)
.await?;
Ok(())
}
/// 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<Option<(u32, String)>, sqlx::Error> {
let user = sqlx::query!("
pub async fn find_user(
&self,
username: &str,
password: &str,
) -> Result<Option<(u32, String)>, sqlx::Error> {
let user = sqlx::query!(
"
SELECT user_id, secret
FROM Users
WHERE username=? AND password=?",
username, password
).fetch_one(&self.0).await?;
username,
password
)
.fetch_one(&self.0)
.await?;
let user_id = user.user_id.unwrap() as u32;
let secret = user.secret.clone();
@ -114,18 +141,27 @@ impl Database {
/// 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!("
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=? AND users.secret=?",
user_id, secret
).fetch_all(&self.0).await?;
user_id,
secret
)
.fetch_all(&self.0)
.await?;
println!("{}", tests.len());
let user_tests = tests.iter()
let user_tests = tests
.iter()
.map(|test| Test {
test_type: test.test_type.clone(),
test_length: test.test_length.unwrap() as u32,
@ -133,32 +169,83 @@ impl Database {
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
accuracy: test.accuracy.unwrap() as u8,
})
.collect();
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<Vec<LeaderBoardTest>, sqlx::Error> {
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?;
)
.fetch_all(&self.0)
.await?;
let leaderboard_tests = tests.iter()
let leaderboard_tests = tests
.iter()
.map(|test| LeaderBoardTest {
username: test.username.clone(),
wpm: test.wpm.unwrap() as u8
wpm: test.wpm.unwrap() as u8,
})
.collect();
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

View File

@ -1,16 +1,10 @@
use crate::typing::sql::Database;
use crate::api::sql::Database;
use rand::{rngs::ThreadRng, Rng};
use rocket::{
serde::{ Deserialize, json::Json },
State
};
use rand::{
Rng,
rngs::ThreadRng
};
use std::{
fs,
vec,
serde::{json::Json, Deserialize},
State,
};
use std::{fs, vec};
/// the datascructure that the webserver will recieve
/// 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 accuracy: u8,
pub user_id: u32,
pub secret: &'r str
pub secret: &'r str,
}
/// 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>) {
let user_id = test.user_id;
match database.create_test(test).await {
Err(why) => { println!("A database error occured creating a test, {why}"); }
Ok(()) => { println!("Successfully created test for {user_id}"); }
Err(why) => {
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,
};
mod typing;
mod api;
mod catchers;
use crate::typing::leaderboard::leaderboard;
use crate::typing::sql::Database;
use crate::typing::test::{create_test, new_test};
use crate::typing::user::{get_tests, login, sign_up};
use crate::api::leaderboard::leaderboard;
use crate::api::sql::Database;
use crate::api::test::{create_test, new_test};
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
@ -66,6 +71,8 @@ async fn rocket() -> Rocket<Build> {
.mount("/test", routes![test])
// hosts the api routes necessary for the website
// to interact with the database
.mount("/api/documentation", FileServer::from(relative!("documentation")))
.register("/api/documentation", catchers![documentation_not_found])
.mount(
"/api",
routes![
@ -77,7 +84,11 @@ async fn rocket() -> Rocket<Build> {
new_test,
],
)
.register("/api", catchers![api_not_found])
// 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())
}

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,
}