From 0e2218d1c1798a1c1a916a612f9c6c83d91b7bab Mon Sep 17 00:00:00 2001 From: ArloFilley Date: Mon, 25 Mar 2024 09:40:43 +0000 Subject: [PATCH] Changed to separate page structure & api updates --- ReadMe.md | 2 +- database/database.sqlite | Bin 16384 -> 16384 bytes documentation/create_test.html | 113 -------- documentation/create_test.md | 54 ---- documentation/create_user.html | 36 --- .../{leaderboard.md => get_leaderboard.md} | 22 +- documentation/get_ping.md | 21 ++ documentation/get_test.md | 39 +++ documentation/get_user_login.md | 31 +++ documentation/get_user_test.md | 30 ++ documentation/get_user_tests.html | 61 ----- documentation/get_user_tests.md | 48 ++-- documentation/html/get_leaderboard.html | 70 +++++ documentation/html/get_ping.html | 39 +++ documentation/html/get_test.html | 57 ++++ documentation/html/get_user_login.html | 52 ++++ documentation/html/get_user_test.html | 47 ++++ documentation/html/get_user_tests.html | 60 ++++ documentation/{ => html}/github-markdown.css | 0 documentation/html/index.html | 39 +++ documentation/html/post_user_create.html | 38 +++ documentation/html/post_user_test.html | 73 +++++ documentation/index.html | 32 --- documentation/index.md | 18 +- documentation/leaderboard.html | 67 ----- documentation/login.html | 56 ---- documentation/login.md | 31 --- documentation/markdown-to-html.sh | 60 +++- .../{create_user.md => post_user_create.md} | 13 +- documentation/post_user_test.md | 42 +++ public/api/api.js | 259 ++++++++---------- public/index.css | 27 -- public/index.js | 69 ----- .../{ => pages}/Admin/Delete User/index.html | 0 public/{ => pages}/Admin/Delete User/index.js | 0 public/{ => pages}/Admin/index.css | 0 public/{ => pages}/Admin/index.html | 0 public/pages/account/index.html | 65 +++++ public/pages/account/script.js | 22 ++ public/pages/account/styles.css | 82 ++++++ public/pages/end/index.html | 26 ++ public/pages/end/script.js | 69 +++++ public/pages/end/styles.css | 85 ++++++ public/pages/leaderboard/index.html | 35 +++ public/{ => pages}/leaderboard/script.js | 0 public/pages/leaderboard/styles.css | 82 ++++++ .../test-settings}/index.html | 8 +- public/pages/test-settings/script.js | 22 ++ .../test-settings}/styles.css | 26 ++ .../assets/favicon/android-chrome-192x192.png | Bin .../assets/favicon/android-chrome-512x512.png | Bin .../test}/assets/favicon/apple-touch-icon.png | Bin .../test}/assets/favicon/favicon-16x16.png | Bin .../test}/assets/favicon/favicon-32x32.png | Bin .../test}/assets/favicon/favicon.ico | Bin .../test}/assets/favicon/site.webmanifest | 0 .../test}/assets/fonts/RobotoMono-Medium.ttf | Bin .../assets/icons/account_circle_black.svg | 0 .../assets/icons/account_circle_white.svg | 0 public/{ => pages/test}/components/button.js | 0 public/{ => pages/test}/components/canvas.js | 4 +- public/{ => pages/test}/components/menu.js | 0 public/{ => pages/test}/components/textbox.js | 0 .../{ => pages/test}/components/timemenu.js | 0 public/{ => pages/test}/components/timer.js | 9 +- public/{ => pages/test}/components/user.js | 0 public/pages/test/index.css | 74 +++++ public/{ => pages/test}/index.html | 21 +- public/pages/test/index.js | 72 +++++ .../{ => pages/test}/screens/screenmanager.js | 0 public/{ => pages/test}/screens/testscreen.js | 10 +- public/screens/accountScreen.js | 115 -------- public/screens/endscreen.js | 36 --- public/screens/leaderboardscreen.js | 101 ------- public/screens/loginscreen.js | 108 -------- public/screens/profilescreen.js | 111 -------- public/screens/settingsScreen.js | 29 -- public/screens/signUpScreen.js | 109 -------- public/screens/startscreen.js | 31 --- src/api/mod.rs | 1 + src/api/ping.rs | 17 ++ src/api/sql.rs | 47 ++-- src/api/test.rs | 67 ++--- src/api/user.rs | 148 ---------- src/api/user/create.rs | 60 ++++ src/api/user/login.rs | 69 +++++ src/api/user/mod.rs | 4 + src/api/user/test.rs | 146 ++++++++++ src/api/user/tests.rs | 74 +++++ src/main.rs | 51 ++-- 90 files changed, 1997 insertions(+), 1645 deletions(-) delete mode 100644 documentation/create_test.html delete mode 100644 documentation/create_test.md delete mode 100644 documentation/create_user.html rename documentation/{leaderboard.md => get_leaderboard.md} (90%) create mode 100644 documentation/get_ping.md create mode 100644 documentation/get_test.md create mode 100644 documentation/get_user_login.md create mode 100644 documentation/get_user_test.md delete mode 100644 documentation/get_user_tests.html create mode 100644 documentation/html/get_leaderboard.html create mode 100644 documentation/html/get_ping.html create mode 100644 documentation/html/get_test.html create mode 100644 documentation/html/get_user_login.html create mode 100644 documentation/html/get_user_test.html create mode 100644 documentation/html/get_user_tests.html rename documentation/{ => html}/github-markdown.css (100%) create mode 100644 documentation/html/index.html create mode 100644 documentation/html/post_user_create.html create mode 100644 documentation/html/post_user_test.html delete mode 100644 documentation/index.html delete mode 100644 documentation/leaderboard.html delete mode 100644 documentation/login.html delete mode 100644 documentation/login.md rename documentation/{create_user.md => post_user_create.md} (66%) create mode 100644 documentation/post_user_test.md delete mode 100755 public/index.css delete mode 100644 public/index.js rename public/{ => pages}/Admin/Delete User/index.html (100%) rename public/{ => pages}/Admin/Delete User/index.js (100%) rename public/{ => pages}/Admin/index.css (100%) rename public/{ => pages}/Admin/index.html (100%) create mode 100644 public/pages/account/index.html create mode 100644 public/pages/account/script.js create mode 100644 public/pages/account/styles.css create mode 100644 public/pages/end/index.html create mode 100644 public/pages/end/script.js create mode 100644 public/pages/end/styles.css create mode 100644 public/pages/leaderboard/index.html rename public/{ => pages}/leaderboard/script.js (100%) create mode 100644 public/pages/leaderboard/styles.css rename public/{leaderboard => pages/test-settings}/index.html (71%) create mode 100644 public/pages/test-settings/script.js rename public/{leaderboard => pages/test-settings}/styles.css (59%) rename public/{ => pages/test}/assets/favicon/android-chrome-192x192.png (100%) rename public/{ => pages/test}/assets/favicon/android-chrome-512x512.png (100%) rename public/{ => pages/test}/assets/favicon/apple-touch-icon.png (100%) rename public/{ => pages/test}/assets/favicon/favicon-16x16.png (100%) rename public/{ => pages/test}/assets/favicon/favicon-32x32.png (100%) rename public/{ => pages/test}/assets/favicon/favicon.ico (100%) rename public/{ => pages/test}/assets/favicon/site.webmanifest (100%) rename public/{ => pages/test}/assets/fonts/RobotoMono-Medium.ttf (100%) rename public/{ => pages/test}/assets/icons/account_circle_black.svg (100%) rename public/{ => pages/test}/assets/icons/account_circle_white.svg (100%) rename public/{ => pages/test}/components/button.js (100%) rename public/{ => pages/test}/components/canvas.js (74%) rename public/{ => pages/test}/components/menu.js (100%) rename public/{ => pages/test}/components/textbox.js (100%) rename public/{ => pages/test}/components/timemenu.js (100%) rename public/{ => pages/test}/components/timer.js (96%) rename public/{ => pages/test}/components/user.js (100%) create mode 100755 public/pages/test/index.css rename public/{ => pages/test}/index.html (70%) create mode 100644 public/pages/test/index.js rename public/{ => pages/test}/screens/screenmanager.js (100%) rename public/{ => pages/test}/screens/testscreen.js (84%) delete mode 100755 public/screens/accountScreen.js delete mode 100755 public/screens/endscreen.js delete mode 100755 public/screens/leaderboardscreen.js delete mode 100755 public/screens/loginscreen.js delete mode 100755 public/screens/profilescreen.js delete mode 100755 public/screens/settingsScreen.js delete mode 100755 public/screens/signUpScreen.js delete mode 100755 public/screens/startscreen.js create mode 100644 src/api/ping.rs delete mode 100644 src/api/user.rs create mode 100644 src/api/user/create.rs create mode 100644 src/api/user/login.rs create mode 100644 src/api/user/mod.rs create mode 100644 src/api/user/test.rs create mode 100644 src/api/user/tests.rs diff --git a/ReadMe.md b/ReadMe.md index 9a89056..2380de6 100644 --- a/ReadMe.md +++ b/ReadMe.md @@ -41,7 +41,7 @@ This project is a web server built using the Rocket framework in Rust. It provid ## Dependencies - **Rocket**: Web framework for Rust. - **Serde**: Serialization and deserialization library for Rust. -- **Rusqlite**: SQLite database driver for Rust. +- **SQLX**: SQLite database driver for Rust. - **Rand**: Random number generation library for Rust. ## Contributors diff --git a/database/database.sqlite b/database/database.sqlite index 609ffcb6ddacb2331d7fb33dc79c1ec43acdc3cc..eaa98b61e67e73f9e3ecc5c4b400169a19de1668 100755 GIT binary patch delta 514 zcmZwDzb`{^6b10!>HCO#5BjC0PeY>VrlX;#q@+~iMnA7o0FE{K2pM#CLqfXB>2ArhN`=b}*45@;o9sZ=bYI53qh{ z?3B`dR9eRRN*ma8no+ue1*elrx3SbT$%&p0^x8s(XKY$!t0=k)DW!F+IUQFTw~Z;? zLdj*V(i#?>Ci^suU#>bH?c-mqV7_7Oh|*n@U3OUM9#)+~X@HuWQvOwrb@p%fc;O)x ME+E%59V&hQ0Gz>KApigX delta 55 zcmZo@U~Fh$oFL68GEv5vQDkGn5`8IdentlV`}`;QH}lWoZ{siEkK%XX*W;JqXWXo) L@R)Bhqy2vXpG6N3 diff --git a/documentation/create_test.html b/documentation/create_test.html deleted file mode 100644 index 54b25bf..0000000 --- a/documentation/create_test.html +++ /dev/null @@ -1,113 +0,0 @@ - - - - - - - -

Post Test Data This API endpoint allows you to post test data, recording the results of a test taken by a user.

-

Endpoint

-
POST /api/post_test
-        
-

Request Parameters

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
ParameterTypeDescription
testTypeStringType of the test (e.g., "typing", "multiple choice")
testLengthIntegerLength of the test in number of items or questions
testTimeIntegerDuration of the test in seconds
testSeedStringSeed for generating randomized test content
quoteIdStringIdentifier for a specific quote, if applicable
wpmIntegerWords per minute (typing speed)
accuracyIntegerAccuracy of responses (e.g., percentage)
userIdStringIdentifier of the user taking the test
secretStringSecret key for authentication and authorization
-

Example Request

-
curl -X POST "https://example.com/api/post_test" \
-            -H "Content-Type: application/json" \ 
-            -d '{   
-        		"testType": "typing",   
-        		"testLength": 100,   
-        		"testTime": 600,   
-        		"testSeed": "random_seed_123",   
-        		"quoteId": "quote_456",   
-        		"wpm": 65.5,   
-        		"accuracy": 98.2,   
-        		"userId": "user_789",   
-        		"secret": "your_secret_key_here" 
-        	}'
-

Example Response

-

Upon successful submission, you will receive a JSON response with the following structure:

-
{
-            "status": "success",   
-            "message": "Test results successfully recorded",   
-            "testId": "test_123456789" 
-        }
-
    -
  • - status: Indicates the status of the request (either "success" or "error").
  • -
  • - message: Describes the outcome of the request.
  • -
  • - testId: Unique identifier assigned to the recorded test data.
  • -
- - \ No newline at end of file diff --git a/documentation/create_test.md b/documentation/create_test.md deleted file mode 100644 index b39ba7c..0000000 --- a/documentation/create_test.md +++ /dev/null @@ -1,54 +0,0 @@ -Post Test Data This API endpoint allows you to post test data, recording the results of a test taken by a user. - -# Endpoint - -``` -POST /api/post_test -``` - -# Request Parameters -| Parameter | Type | Description | -| ---------- | :-----: | ---------------------------------------------------- | -| testType | String | Type of the test (e.g., "typing", "multiple choice") | -| testLength | Integer | Length of the test in number of items or questions | -| testTime | Integer | Duration of the test in seconds | -| testSeed | String | Seed for generating randomized test content | -| quoteId | String | Identifier for a specific quote, if applicable | -| wpm | Integer | Words per minute (typing speed) | -| accuracy | Integer | Accuracy of responses (e.g., percentage) | -| userId | String | Identifier of the user taking the test | -| secret | String | Secret key for authentication and authorization | - -# Example Request - -```bash -curl -X POST "https://example.com/api/post_test" \ - -H "Content-Type: application/json" \ - -d '{ - "testType": "typing", - "testLength": 100, - "testTime": 600, - "testSeed": "random_seed_123", - "quoteId": "quote_456", - "wpm": 65.5, - "accuracy": 98.2, - "userId": "user_789", - "secret": "your_secret_key_here" - }' -``` - -# Example Response - -Upon successful submission, you will receive a JSON response with the following structure: - -```json -{ - "status": "success", - "message": "Test results successfully recorded", - "testId": "test_123456789" -} -``` - -- `status`: Indicates the status of the request (either "success" or "error"). -- `message`: Describes the outcome of the request. -- `testId`: Unique identifier assigned to the recorded test data. diff --git a/documentation/create_user.html b/documentation/create_user.html deleted file mode 100644 index 86e2b5b..0000000 --- a/documentation/create_user.html +++ /dev/null @@ -1,36 +0,0 @@ - - - - - - - -

Create User

-

Creates a new user in the database. - Endpoint

-
POST /api/create_user
-

Request Body

-
{
-        "username": "example_user",
-        "password": "example_password"
-        
}
-

Example Request

-
curl -X POST "https://example.com/api/create_user" \
-        -H "Content-Type: application/json" \
-        -d '{"username": "example_user", "password": "example_password"}'
- - \ No newline at end of file diff --git a/documentation/leaderboard.md b/documentation/get_leaderboard.md similarity index 90% rename from documentation/leaderboard.md rename to documentation/get_leaderboard.md index a4d14ae..29947b5 100644 --- a/documentation/leaderboard.md +++ b/documentation/get_leaderboard.md @@ -4,12 +4,27 @@ Returns the highest test data from each user as a JSON array. The data includes metrics such as username, words per minute (WPM), accuracy percentage, the time taken for the test, and the length of the test for a comprehensive overview of user performance. +## Request Parameters + +- `username`: The name of the user. +- `wpm`: Words per minute, indicating the typing speed. +- `accuracy`: The accuracy of the user's typing, in percentage. +- `test_time`: The total time taken to complete the test, in seconds. +- `test_length`: The length of the test, typically measured in number of words. + ## Responses - `200 OK`: Successfully retrieves the leaderboard data. - `404 Not Found`: Indicates that the leaderboard was not found. - `500 Internal Server Error`: Indicates an issue with accessing the database. + +## Example Request + +```bash +curl -X GET "https://url/api/leaderboard" +``` + ## Example Response ```json @@ -31,10 +46,3 @@ Returns the highest test data from each user as a JSON array. The data includes ] ``` -## Fields - -- `username`: The name of the user. -- `wpm`: Words per minute, indicating the typing speed. -- `accuracy`: The accuracy of the user's typing, in percentage. -- `test_time`: The total time taken to complete the test, in seconds. -- `test_length`: The length of the test, typically measured in number of words. \ No newline at end of file diff --git a/documentation/get_ping.md b/documentation/get_ping.md new file mode 100644 index 0000000..5d71822 --- /dev/null +++ b/documentation/get_ping.md @@ -0,0 +1,21 @@ +# Ping API Endpoint + +## GET `/api/ping` + +Shows whether the api is up and running. Accessible from http://url/api/ping + +## Responses + +- `200 OK`: The api is up and running + +## Example Request + +```bash +curl -X GET "https://url/api/ping +``` + +## Example Response + +``` +"Hello World! I'm A rocket Webserver" +``` \ No newline at end of file diff --git a/documentation/get_test.md b/documentation/get_test.md new file mode 100644 index 0000000..7169be7 --- /dev/null +++ b/documentation/get_test.md @@ -0,0 +1,39 @@ +# Test API Endpoint + +## GET `/api/test` + +Returns 100 random words to be used to give a typing test. Retrieved from `wordlist.txt` + +## Responses + +- `200 OK`: Successfully retrieves the 100 words. +- `404 Not Found`: Indicates that the wordlist was not found. + +## Example Request + +```bash +curl -X GET "https://url/api/test" +``` + +## Example Response + +```json +[ + "separate", + "stand", + "think", + "island", + "have", + "air", + "heard", + "notice", + "yellow", + "smell", + "heart", + "island", + "chief", + "view", + "top", + ... +] +``` diff --git a/documentation/get_user_login.md b/documentation/get_user_login.md new file mode 100644 index 0000000..961583e --- /dev/null +++ b/documentation/get_user_login.md @@ -0,0 +1,31 @@ +# User Login Endpoint + +## GET `/api/user/login//` + +Takes the user's login information and returns the user's user ID, which can be used to identify their tests, etc. Accessible from http://url/api/login. + +## Request Parameters + +- `username`: The username of the user. +- `password`: The password of the user. + +## Responses + +- `200 OK`: Successfully retrieves the user ID. +- `401 Unauthorized`: Indicates that the login credentials are invalid. +- `500 Internal Server Error`: Indicates an issue with accessing the database. + +## Example Request + +```bash +curl -X GET "https://url/api/user/login/example_user/example_password" +``` + +## Example Response + +```json +{ + "user_id": "1234567890", + "secret": "abcdefghijklmnopqrstuvwxyz" +} +``` \ No newline at end of file diff --git a/documentation/get_user_test.md b/documentation/get_user_test.md new file mode 100644 index 0000000..cb15291 --- /dev/null +++ b/documentation/get_user_test.md @@ -0,0 +1,30 @@ +# Get User Test + +## GET `/api/user/test//` + +Retrieves tests associated with a specific user from the database. + +## Parameters + +- `user_id` - User ID of the user whose tests need to be retrieved +- `secret` - Secret key for authentication + +## Example Request + +```bash +curl -X GET "https://url/api/user/test/123/your_secret_key_here" +``` + +## Example Response + +```json +{ + "test_type": "words", + "test_length": 100, + "test_time": 300, + "test_seed": 987654321, + "quote_id": 123, + "wpm": 65, + "accuracy": 98 +} +``` \ No newline at end of file diff --git a/documentation/get_user_tests.html b/documentation/get_user_tests.html deleted file mode 100644 index be5950e..0000000 --- a/documentation/get_user_tests.html +++ /dev/null @@ -1,61 +0,0 @@ - - - - - - - -

Get User Tests

-

Retrieves tests associated with a specific user from the database.

-

Endpoint

-
GET /api/get_tests/<user_id>/<secret>
-

Parameters

- - - - - - - - - - - - - - - - - - - - -
ParameterTypeDescription
user_idintegerUser ID of the user whose tests need to be retrieved
secretStringSecret key for authentication
-

Example Request

-
curl -X GET "https://example.com/api/get_tests/123/your_secret_key_here"
-

Example Response

-
{
-        "test_type": "words",
-        "test_length": 100,
-        "test_time": 300,
-        "test_seed": 987654321,
-        "quote_id": 123,
-        "wpm": 65,
-        "accuracy": 98
-        
}
- - \ No newline at end of file diff --git a/documentation/get_user_tests.md b/documentation/get_user_tests.md index 36d1677..a216d85 100644 --- a/documentation/get_user_tests.md +++ b/documentation/get_user_tests.md @@ -1,36 +1,42 @@ # Get User Tests +## GET `/api/user/tests//` + Retrieves tests associated with a specific user from the database. -## Endpoint +## Request Parameters -```js -GET /api/get_tests// -``` - -## Parameters - -| Parameter | Type | Description | -| - | - | - | -| user_id | integer | User ID of the user whose tests need to be retrieved | -| secret | String | Secret key for authentication | +- `user_id` - User ID of the user whose tests need to be retrieved +- `secret` - Secret key for authentication ## Example Request ```bash -curl -X GET "https://example.com/api/get_tests/123/your_secret_key_here" +curl -X GET "https://url/api/user/tests/123/your_secret_key_here" ``` ## Example Response ```json -{ - "test_type": "words", - "test_length": 100, - "test_time": 300, - "test_seed": 987654321, - "quote_id": 123, - "wpm": 65, - "accuracy": 98 -} +[ + { + "test_type": "words", + "test_length": 300, + "test_time": 60, + "test_seed": 0, + "quote_id": 0, + "wpm": 60, + "accuracy": 100 + }, + { + "test_type": "words", + "test_length": 47, + "test_time": 15, + "test_seed": 0, + "quote_id": 0, + "wpm": 37, + "accuracy": 98 + }, + ... +] ``` \ No newline at end of file diff --git a/documentation/html/get_leaderboard.html b/documentation/html/get_leaderboard.html new file mode 100644 index 0000000..bc50b73 --- /dev/null +++ b/documentation/html/get_leaderboard.html @@ -0,0 +1,70 @@ + + + + + + + + +

Leaderboard API Endpoint

+

GET /api/leaderboard +

+

Returns the highest test data from each user as a JSON array. The data includes metrics such as username, words per minute (WPM), accuracy percentage, the time taken for the test, and the length of the test for a comprehensive overview of user performance.

+

Request Parameters

+
    +
  • +username: The name of the user.
  • +
  • +wpm: Words per minute, indicating the typing speed.
  • +
  • +accuracy: The accuracy of the user's typing, in percentage.
  • +
  • +test_time: The total time taken to complete the test, in seconds.
  • +
  • +test_length: The length of the test, typically measured in number of words.
  • +
+

Responses

+
    +
  • +200 OK: Successfully retrieves the leaderboard data.
  • +
  • +404 Not Found: Indicates that the leaderboard was not found.
  • +
  • +500 Internal Server Error: Indicates an issue with accessing the database.
  • +
+

Example Request

+
curl -X GET "https://url/api/leaderboard"
+

Example Response

+
[
+  {
+    "username": "user1",
+    "wpm": 75,
+    "accuracy": 97,
+    "test_time": 120,
+    "test_length": 250
+  },
+  {
+    "username": "user2",
+    "wpm": 73,
+    "accuracy": 95,
+    "test_time": 115,
+    "test_length": 240
+  }
+]
+ + + diff --git a/documentation/html/get_ping.html b/documentation/html/get_ping.html new file mode 100644 index 0000000..ee1f0a4 --- /dev/null +++ b/documentation/html/get_ping.html @@ -0,0 +1,39 @@ + + + + + + + + +

Ping API Endpoint

+

GET /api/ping +

+

Shows whether the api is up and running. Accessible from http://url/api/ping

+

Responses

+
    +
  • +200 OK: The api is up and running
  • +
+

Example Request

+
curl -X GET "https://url/api/ping
+

Example Response

+
"Hello World! I'm A rocket Webserver"
+
+ + + diff --git a/documentation/html/get_test.html b/documentation/html/get_test.html new file mode 100644 index 0000000..a072a0b --- /dev/null +++ b/documentation/html/get_test.html @@ -0,0 +1,57 @@ + + + + + + + + +

Test API Endpoint

+

GET /api/test +

+

Returns 100 random words to be used to give a typing test. Retrieved from wordlist.txt

+

Responses

+
    +
  • +200 OK: Successfully retrieves the 100 words.
  • +
  • +404 Not Found: Indicates that the wordlist was not found.
  • +
+

Example Request

+
curl -X GET "https://url/api/test"
+

Example Response

+
[
+  "separate",
+  "stand",
+  "think",
+  "island",
+  "have",
+  "air",
+  "heard",
+  "notice",
+  "yellow",
+  "smell",
+  "heart",
+  "island",
+  "chief",
+  "view",
+  "top",
+  ...
+]
+ + + diff --git a/documentation/html/get_user_login.html b/documentation/html/get_user_login.html new file mode 100644 index 0000000..cc0bf98 --- /dev/null +++ b/documentation/html/get_user_login.html @@ -0,0 +1,52 @@ + + + + + + + + +

User Login Endpoint

+

GET /api/user/login/<username>/<password> +

+

Takes the user's login information and returns the user's user ID, which can be used to identify their tests, etc. Accessible from http://url/api/login.

+

Request Parameters

+
    +
  • +username: The username of the user.
  • +
  • +password: The password of the user.
  • +
+

Responses

+
    +
  • +200 OK: Successfully retrieves the user ID.
  • +
  • +401 Unauthorized: Indicates that the login credentials are invalid.
  • +
  • +500 Internal Server Error: Indicates an issue with accessing the database.
  • +
+

Example Request

+
curl -X GET "https://url/api/user/login/example_user/example_password"
+

Example Response

+
{
+  "user_id": "1234567890",
+  "secret": "abcdefghijklmnopqrstuvwxyz"
+}
+ + + diff --git a/documentation/html/get_user_test.html b/documentation/html/get_user_test.html new file mode 100644 index 0000000..c67b85e --- /dev/null +++ b/documentation/html/get_user_test.html @@ -0,0 +1,47 @@ + + + + + + + + +

Get User Test

+

GET /api/user/test/<user_id>/<secret>

+

Retrieves tests associated with a specific user from the database.

+

Parameters

+
    +
  • +user_id - User ID of the user whose tests need to be retrieved
  • +
  • +secret - Secret key for authentication
  • +
+

Example Request

+
curl -X GET "https://url/api/user/test/123/your_secret_key_here"
+

Example Response

+
{
+  "test_type": "words",
+  "test_length": 100,
+  "test_time": 300,
+  "test_seed": 987654321,
+  "quote_id": 123,
+  "wpm": 65,
+  "accuracy": 98
+}
+ + + diff --git a/documentation/html/get_user_tests.html b/documentation/html/get_user_tests.html new file mode 100644 index 0000000..022bc28 --- /dev/null +++ b/documentation/html/get_user_tests.html @@ -0,0 +1,60 @@ + + + + + + + + +

Get User Tests

+

GET /api/user/tests/<user_id>/<secret> +

+

Retrieves tests associated with a specific user from the database.

+

Request Parameters

+
    +
  • +user_id - User ID of the user whose tests need to be retrieved
  • +
  • +secret - Secret key for authentication
  • +
+

Example Request

+
curl -X GET "https://url/api/user/tests/123/your_secret_key_here"
+

Example Response

+
[
+  {
+    "test_type": "words",
+    "test_length": 300,
+    "test_time": 60,
+    "test_seed": 0,
+    "quote_id": 0,
+    "wpm": 60,
+    "accuracy": 100
+  },
+  {
+    "test_type": "words",
+    "test_length": 47,
+    "test_time": 15,
+    "test_seed": 0,
+    "quote_id": 0,
+    "wpm": 37,
+    "accuracy": 98
+  },
+  ...
+]
+ + + diff --git a/documentation/github-markdown.css b/documentation/html/github-markdown.css similarity index 100% rename from documentation/github-markdown.css rename to documentation/html/github-markdown.css diff --git a/documentation/html/index.html b/documentation/html/index.html new file mode 100644 index 0000000..7b5f522 --- /dev/null +++ b/documentation/html/index.html @@ -0,0 +1,39 @@ + + + + + + + + +

Routes

+

/api

+

/user

+ + + + +

/Documentation

+ + + diff --git a/documentation/html/post_user_create.html b/documentation/html/post_user_create.html new file mode 100644 index 0000000..75695bd --- /dev/null +++ b/documentation/html/post_user_create.html @@ -0,0 +1,38 @@ + + + + + + + + +

Create User API Endpoint

+

POST /api/user/create +

+

Creates a new user in the database.

+

Request Parameters

+
{
+  "username": "example_user",
+  "password": "example_password"
+}
+

Example Request

+
curl -X POST "https://url/api/user/create" \
+-H "Content-Type: application/json" \
+-d '{"username": "example_user", "password": "example_password"}'
+ + + diff --git a/documentation/html/post_user_test.html b/documentation/html/post_user_test.html new file mode 100644 index 0000000..d580b5f --- /dev/null +++ b/documentation/html/post_user_test.html @@ -0,0 +1,73 @@ + + + + + + + + +

Create Test API Endpoint

+

POST /api/post_test +

+

Post Test Data This API endpoint allows you to post test data, recording the results of a test taken by a user.

+

Request Parameters

+
    +
  • +testType - Type of the test ("words", "time", "quote", etc)
  • +
  • +testLength - Length of the test in number of items or
  • +
  • +testTime - Duration of the test in seconds
  • +
  • +testSeed - Seed for generating randomized test content
  • +
  • +quoteId - Identifier for a specific quote, if applicable
  • +
  • +wpm - Words per minute (typing speed)
  • +
  • +accuracy - Accuracy of responses (e.g., percentage)
  • +
  • +userId - Identifier of the user taking the test
  • +
  • +secret - Secret key for authentication and authorization
  • +
+

Responses

+
    +
  • +200 OK - The test has been added to the database sucessfully
  • +
  • +401 UNAUTHORIZED - The user has not been authenticated correctly
  • +
  • +500 INTERNAL SERVER ERROR - There has been a database error when attempting to
  • +
+

Example Request

+
curl -X POST "https://example.com/api/post_test" \
+	-H "Content-Type: application/json" \ 
+	-d '{   
+		"testType": "typing",   
+		"testLength": 100,   
+		"testTime": 600,   
+		"testSeed": "random_seed_123",   
+		"quoteId": "quote_456",   
+		"wpm": 65.5,   
+		"accuracy": 98.2,   
+		"userId": "user_789",   
+		"secret": "your_secret_key_here" 
+	}'
+ + + diff --git a/documentation/index.html b/documentation/index.html deleted file mode 100644 index aa49121..0000000 --- a/documentation/index.html +++ /dev/null @@ -1,32 +0,0 @@ - - - - - - - - -

Links

- - - \ No newline at end of file diff --git a/documentation/index.md b/documentation/index.md index 4e5348d..a734605 100644 --- a/documentation/index.md +++ b/documentation/index.md @@ -1,7 +1,13 @@ -# Links +# Routes -- [POST `/api/Create_User`](./create_user.md) -- [POST `/api/Post_Test`](./create_test.md) -- [GET `/api/Get_User_Tests`](./get_user_tests.md) -- [GET `/api/Leaderboard`](./leaderboard.md) -- [GET `/api/Login`](./login.md) +## `/api` +### `/user` +- [POST `/create`](./post_user_create.md) +- [GET `/login`](./get_user_login.md) +- [GET `/tests`](./get_user_tests.md) +- [POST `/test`](./post_user_test.md) +- [GET `/test`](./get_user_test.md) +### [GET `/ping`](./get_ping.md) +### [GET `/leaderboard`](./get_leaderboard.md) +### [GET `/test`](./get_test.md) +### `/Documentation` \ No newline at end of file diff --git a/documentation/leaderboard.html b/documentation/leaderboard.html deleted file mode 100644 index e2c4045..0000000 --- a/documentation/leaderboard.html +++ /dev/null @@ -1,67 +0,0 @@ - - - - - - - - -

Leaderboard API Endpoint

-

GET /api/leaderboard -

-

Returns the highest test data from each user as a JSON array. The data includes metrics such as username, words per minute (WPM), accuracy percentage, the time taken for the test, and the length of the test for a comprehensive overview of user performance.

-

Responses

-
    -
  • - 200 OK: Successfully retrieves the leaderboard data.
  • -
  • - 404 Not Found: Indicates that the leaderboard was not found.
  • -
  • - 500 Internal Server Error: Indicates an issue with accessing the database.
  • -
-

Example Response

-
[
-          {
-            "username": "user1",
-            "wpm": 75,
-            "accuracy": 97,
-            "test_time": 120,
-            "test_length": 250
-          },
-          {
-            "username": "user2",
-            "wpm": 73,
-            "accuracy": 95,
-            "test_time": 115,
-            "test_length": 240
-          }
-]
-

Fields

-
    -
  • - username: The name of the user.
  • -
  • - wpm: Words per minute, indicating the typing speed.
  • -
  • - accuracy: The accuracy of the user's typing, in percentage.
  • -
  • - test_time: The total time taken to complete the test, in seconds.
  • -
  • - test_length: The length of the test, typically measured in number of words.
  • -
- - \ No newline at end of file diff --git a/documentation/login.html b/documentation/login.html deleted file mode 100644 index dd8cfe5..0000000 --- a/documentation/login.html +++ /dev/null @@ -1,56 +0,0 @@ - - - - - - - -

Login

-

Authenticates a user and returns their user ID along with a secret key. - Endpoint

-
GET /api/login/<username>/<password>
-

Parameters

- - - - - - - - - - - - - - - - - - - - -
ParameterTypeDescription
usernameStringUsername of the user
passwordStringPassword of the user
-

Example Request

-
curl -X GET "https://example.com/api/login/example_user/example_password"
-

Example Response

-
{
-        "user_id": 123,
-        "secret": "random_secret_key"
-        }
- - \ No newline at end of file diff --git a/documentation/login.md b/documentation/login.md deleted file mode 100644 index 17e66d7..0000000 --- a/documentation/login.md +++ /dev/null @@ -1,31 +0,0 @@ -# Login - -Authenticates a user and returns their user ID along with a secret key. -Endpoint - - -```js -GET /api/login// -``` - -## Parameters - -| Parameter | Type | Description | -| - | - | - | -| username | String |Username of the user | -| password | String |Password of the user | - -## Example Request - -```bash -curl -X GET "https://example.com/api/login/example_user/example_password" -``` - -## Example Response - -```json -{ - "user_id": 123, - "secret": "random_secret_key" -} -``` \ No newline at end of file diff --git a/documentation/markdown-to-html.sh b/documentation/markdown-to-html.sh index 12c318a..1e8ef68 100755 --- a/documentation/markdown-to-html.sh +++ b/documentation/markdown-to-html.sh @@ -1,18 +1,52 @@ #!/bin/bash -# Read markdown content from file -markdown_content=$(cat $1) +# HTML structure +html_start=' + + + + + + + +' -# Write HTML content to file -echo "$html_content" > $2 +html_end=' + + +' -echo "Conversion completed. HTML content written to $2" +# Iterate over each markdown file in the current directory +for file in *.md; do + # Read markdown content from file + markdown_content=$(cat "$file") + + # Convert markdown to HTML + html_content=$(gh api \ + --method POST \ + -H "Accept: application/vnd.github+json" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + /markdown \ + -f text="$markdown_content" + ) + + # Write HTML content to a new file + html_file="./html/${file%.md}.html" + echo "$html_start$html_content$html_end" > "$html_file" + + echo "Conversion completed. HTML content written to $html_file" +done \ No newline at end of file diff --git a/documentation/create_user.md b/documentation/post_user_create.md similarity index 66% rename from documentation/create_user.md rename to documentation/post_user_create.md index 9ac469b..5125bdd 100644 --- a/documentation/create_user.md +++ b/documentation/post_user_create.md @@ -1,13 +1,10 @@ -# Create User +# Create User API Endpoint + +## POST `/api/user/create` Creates a new user in the database. -Endpoint -```js -POST /api/create_user -``` - -## Request Body +## Request Parameters ```json { @@ -19,7 +16,7 @@ POST /api/create_user ## Example Request ```bash -curl -X POST "https://example.com/api/create_user" \ +curl -X POST "https://url/api/user/create" \ -H "Content-Type: application/json" \ -d '{"username": "example_user", "password": "example_password"}' ``` \ No newline at end of file diff --git a/documentation/post_user_test.md b/documentation/post_user_test.md new file mode 100644 index 0000000..16586c5 --- /dev/null +++ b/documentation/post_user_test.md @@ -0,0 +1,42 @@ +# Create Test API Endpoint + +## POST `/api/post_test` + +Post Test Data This API endpoint allows you to post test data, recording the results of a test taken by a user. + +## Request Parameters + +- `testType` - Type of the test ("words", "time", "quote", etc) +- `testLength` - Length of the test in number of items or +- `testTime` - Duration of the test in seconds +- `testSeed` - Seed for generating randomized test content +- `quoteId` - Identifier for a specific quote, if applicable +- `wpm` - Words per minute (typing speed) +- `accuracy` - Accuracy of responses (e.g., percentage) +- `userId` - Identifier of the user taking the test +- `secret` - Secret key for authentication and authorization + +## Responses + +- `200 OK` - The test has been added to the database sucessfully +- `401 UNAUTHORIZED` - The user has not been authenticated correctly +- `500 INTERNAL SERVER ERROR` - There has been a database error when attempting to + +## Example Request + +```bash +curl -X POST "https://example.com/api/post_test" \ + -H "Content-Type: application/json" \ + -d '{ + "testType": "typing", + "testLength": 100, + "testTime": 600, + "testSeed": "random_seed_123", + "quoteId": "quote_456", + "wpm": 65.5, + "accuracy": 98.2, + "userId": "user_789", + "secret": "your_secret_key_here" + }' +``` + diff --git a/public/api/api.js b/public/api/api.js index 3368602..d449c96 100644 --- a/public/api/api.js +++ b/public/api/api.js @@ -8,155 +8,102 @@ * This class provides all the useful methods to interact with the api. */ class API { - constructor() { this.url = "/api"; } - /** - * This takes the validated data and makes a post - * request to the rocket server - * @param {String} testType - * @param {int} testLength - * @param {int} testTime - * @param {int} testSeed - * @param {int} quoteId - * @param {int} wpm - * @param {int} accuracy - * @param {int} userId - */ - postTest(pTestType, pTestLength, pTestTime, pTestSeed, pQuoteId, pWpm, pAccuracy, pUserId) { - const data = { - 'test_type': pTestType, - 'test_length': pTestLength, - 'test_time': pTestTime, - 'test_seed': pTestSeed, - 'quote_id': pQuoteId, - 'wpm': pWpm, - 'accuracy': pAccuracy, - 'user_id': pUserId, - 'secret': user.secret - } - - const xhr = new XMLHttpRequest(); - xhr.open( - "POST", - `${this.url}/post_test/` - ); - - xhr.send( - JSON.stringify(data) - ); - - user.lastTest = data; - } - - /** - * Validates all the parameters used for the postTest function which it then calls - */ - validateTest() { - const test = screenManager.screen.textbox.getWords(); + async createTest() { + const test = screenManager.screen.textbox.getLetters(); const testType = "words"; - let testLength = test.length; - let testTime = screenManager.screen.timer.getTime(); + const testLength = test.length; + const testTime = screenManager.screen.timer.getTime(); const testSeed = 0; const quoteId = 0; - let wpm; const userId = Number(user.userId); - let test_content = screenManager.screen.textbox.getTestContent(); + const testContent = screenManager.screen.textbox.getTestContent(); - let string = ""; - let inaccurateLetters = 0; - for (let letter = 0; letter < test.length; letter++) { - if (test[letter] === test_content[letter]) { - string += test[letter]; - } else { - inaccurateLetters += 1; + let correctLetters = 0; + test.forEach((letter, index) => { + if (letter === testContent[index]) correctLetters++; + }); + + const accuracy = Math.round((correctLetters / test.length) * 100); + const wpm = Math.round((correctLetters / 5) * (60 / testTime)); + + const validations = [ + {value: testType, type: "string"}, + {value: testLength, type: "number", min: 0}, + {value: testTime, type: "number", min: 1}, + {value: testSeed, type: "number", min: 0}, + {value: quoteId, type: "number", min: 0}, + {value: wpm, type: "number", min: 0}, + {value: accuracy, type: "number", min: 0, max: 100}, + {value: userId, type: "number", min: 0} + ]; + + for (let {value, type, min, max} of validations) { + if (typeof value !== type || value < min || (max !== undefined && value > max)) { + console.error(`Validation failed for value: ${value}, Type: ${type}, Min: ${min}, Max: ${max}`); + return; } } - const accuracy = Math.round(((test.length - inaccurateLetters) / test.length) * 100); + const data = { + test_type: testType, + test_length: testLength, + test_time: testTime, + test_seed: testSeed, + quote_id: quoteId, + wpm: wpm, + accuracy: accuracy, + user_id: userId, + secret: user.secret + }; - // this is the wpm calculation factoring in the time of test - // it assumes that all words are 5 characters long because on average - // they are - wpm = Math.round((string.length / 5) * (60 / testTime)); + try { + const response = await fetch(`${this.url}/user/test`, { + method: "POST", + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(data) + }); + + if (!response.ok) { + throw new Error('Network response was not ok'); + } - // the following code is a series of if statements that checks the - // types of the variables is correct if not it errors it and returns - // out of the function + // const responseData = await response.json(); + // console.log(responseData); + user.lastTest = data; // Consider updating user.lastTest only on successful response + window.location.href = "/typing/pages/end/index.html" + } catch (error) { + console.error('There has been a problem with your fetch operation:', error); + } + } - if ( typeof testType !== "string" ) { - console.error(`testType is value ${typeof testType}\nshould be a string`); - return; + /** + * Fetches the latest test for a given user. + * @param {number} userId The user's ID. + * @param {string} secret The secret key for authentication. + * @returns {Promise} The latest test object or an error message. + */ + async getLatestUserTest() { + let userId = Number(user.userId); + let secret = user.secret; + const url = `${this.url}/user/test/${userId}/${secret}`; + + try { + const response = await fetch(url); + if (!response.ok) { + // Handle HTTP errors + throw new Error(`HTTP error! status: ${response.status}`); + } + const data = await response.json(); + console.log("Successfully fetched latest test:", data); + return data; + } catch (error) { + console.error("Error fetching the latest user test:", error); + return null; // or handle as needed } - if ( typeof testLength !== "number") { - console.error(`testLength is value ${typeof testLength}\n should be a number`); - return; - } - if ( typeof testTime !== "number") { - console.error(`testTime is value ${typeof testTime}\n should be a number`); - return; - } - if ( typeof testSeed !== "number") { - console.error(`testSeed is value ${typeof testSeed}\n should be a number`); - return; - } - if ( typeof quoteId !== "number") { - console.error(`quoteId is value ${typeof quoteId}\n should be a number`); - return; - } - if ( typeof wpm !== "number") { - console.error(`wpm is value ${typeof wpm}\n should be a number`); - return; - } - if ( typeof accuracy !== "number") { - console.error(`accuracy is value ${typeof accuracy}\n should be a number`); - return; - } - if ( typeof userId !== "number") { - console.error(`userId is value ${typeof userId}\n should be a number`); - return; - } - - // after checking that all variables are of the correct type these if statements check - // that they are acceptable values or are in acceptable bounds depending on variable types - - if (testType !== "words") { - // currently words is the only acceptable type but - // this will change in later iterations - - console.error(`testType is invalid\nacceptable options ['words']`); - } - // upper bounds for these numbers are less of a concern because the server will automatically - // return an error if values are over the limit - if (testLength < 0) { - console.error(`testLength is too small, min value 0`) - } - if (testTime < 1) { - console.error(`testTime is too small, min value 1`) - } - if (testSeed < 0) { - console.error(`testSeed is too small, min value 0`) - } - if (quoteId < 0) { - console.error(`quoteId is too small, min value 0`) - } - if (wpm < 0) { - console.error(`wpm is too small, min value 0`) - } - // accuracy needs an upper bound check because users can't have more than 100% - // accuracy when completing their tests - if (accuracy < 0) { - console.error(`accuracy is too small, min value 0`) - } else if (accuracy > 100) { - console.error(`accuracy is too big, max value 100`) - } - if (userId < 0) { - console.error(`userId is too small, min value 0`) - } - - // there will be other tests here in later iterations but for now these tests should suffice - - this.postTest(testType, testLength, testTime, testSeed, quoteId, wpm, accuracy, userId); } /** @@ -175,7 +122,7 @@ class API { }; const xhr = new XMLHttpRequest(); - xhr.open( "POST", `${this.url}/create_user/` ); + xhr.open( "POST", `${this.url}/user/create/` ); xhr.send( JSON.stringify(user) ); @@ -215,7 +162,7 @@ class API { } let xhr = new XMLHttpRequest(); - xhr.open('GET', `${this.url}/login/${pUsername}/${pPassword}`); + xhr.open('GET', `${this.url}/user/login/${pUsername}/${pPassword}`); xhr.send(); xhr.onload = () => { let response = JSON.parse(xhr.response); @@ -253,29 +200,35 @@ class API { } let xhr = new XMLHttpRequest(); - xhr.open('GET', `${this.url}/get_tests/${user.userId}/${user.secret}`); + xhr.open('GET', `${this.url}/user/tests/${user.userId}/${user.secret}`); xhr.send(); xhr.onload = () => { user.tests = JSON.parse(xhr.response); }; } - getLeaderBoard() { - let xhr = new XMLHttpRequest(); - xhr.open('GET', `${this.url}/leaderboard/`); - xhr.send(); - xhr.onload = () => { - user.leaderboard = JSON.parse(xhr.response); - }; + async getLeaderBoard() { + try { + const response = await fetch(`${this.url}/leaderboard/`); + if (!response.ok) { + throw new Error('Network response was not ok.'); + } + user.leaderboard = await response.json(); + } catch (error) { + console.error('Failed to fetch leaderboard:', error); + } } + + + async getTest() { + try { + const response = await fetch(`${this.url}/test/`); + if (!response.ok) { + throw new Error('Network response was not ok.'); + } - getTest() { - let xhr = new XMLHttpRequest(); - xhr.open('GET', `${this.url}/new_test/`); - xhr.send(); - xhr.onload = () =>{ const effectiveWidth = (windowWidth - 200) / 13; - let textArr = JSON.parse(xhr.response); + let textArr = await response.json(); let finalText = []; let text = ""; for (let i = 0; i < textArr.length; i++) { @@ -287,6 +240,8 @@ class API { } } user.nextTest = finalText; - }; + } catch (error) { + console.error('Failed to fetch a test:', error); + } } } \ No newline at end of file diff --git a/public/index.css b/public/index.css deleted file mode 100755 index b7ac9cf..0000000 --- a/public/index.css +++ /dev/null @@ -1,27 +0,0 @@ -/* -Index.css -Description: This file is the stylesheet for the html that the - user will see if they do not have javascript enabled. -Author: Arlo Filley -*/ - -:root { - --background-color: #000000; - --text-color: yellow; - --font: Verdana, Geneva, Tahoma, sans-serif; -} - -body { - background-color: var(--background-color); - position: absolute; - margin: 0; -} - -noscript { - display: block; - text-align: center; - - font-family: var(--font); - color: var(--text-color); - font-size: large; -} \ No newline at end of file diff --git a/public/index.js b/public/index.js deleted file mode 100644 index af54c6b..0000000 --- a/public/index.js +++ /dev/null @@ -1,69 +0,0 @@ -/** - * @file This files is the root of the website. - * @author Arlo Filley - */ - -// these are all of the globally accessible variables that are -// needed for the site to run correctly -let canvas, api, screenManager, user; - -/** - * loads the any assets before the setup function - * this allows p5.js to acess these assets including: sprites, - * fonts, etc -*/ -function preload() { - roboto = loadFont('./assets/fonts/RobotoMono-Medium.ttf'); - accountIconBlack = loadImage('./assets/icons/account_circle_black.svg'); - accountIconWhite = loadImage('./assets/icons/account_circle_white.svg'); -} - -/** - * defines variables and sets up the p5.js canvas - * ready to be drawn with using the draw() function -*/ -function setup() { - canvas = new Canvas(); - canvas.resize(); - canvas.center(); - - frameRate(60); - - api = new API(); - screenManager = new ScreenManager(); - user = new User(); - screenManager.setScreen(new StartScreen()); - - api.login(null, null, true); - api.getTest(); - textFont(roboto); -} - - -/** - * called once per frame. draws all other elements onto the canvas - * mostly will just call the screenManager.draw() method to make - * sure that the correct screen is being drawn -*/ -function draw() { - background(user.colorScheme.background); - screenManager.draw(); -} - -/** - * called whenever a key is pressed, the variable key contains the - * key that the user last pressed -*/ -function keyPressed() { - screenManager.letterTyped(key); -} - - -/** - * called whenever the user resizes the window. Uses methods from the canvas wrapper class - * to resize and center the canvas such that it displays correctly -*/ -function windowResized() { - canvas.resize(); - canvas.center(); -} \ No newline at end of file diff --git a/public/Admin/Delete User/index.html b/public/pages/Admin/Delete User/index.html similarity index 100% rename from public/Admin/Delete User/index.html rename to public/pages/Admin/Delete User/index.html diff --git a/public/Admin/Delete User/index.js b/public/pages/Admin/Delete User/index.js similarity index 100% rename from public/Admin/Delete User/index.js rename to public/pages/Admin/Delete User/index.js diff --git a/public/Admin/index.css b/public/pages/Admin/index.css similarity index 100% rename from public/Admin/index.css rename to public/pages/Admin/index.css diff --git a/public/Admin/index.html b/public/pages/Admin/index.html similarity index 100% rename from public/Admin/index.html rename to public/pages/Admin/index.html diff --git a/public/pages/account/index.html b/public/pages/account/index.html new file mode 100644 index 0000000..5b746c4 --- /dev/null +++ b/public/pages/account/index.html @@ -0,0 +1,65 @@ + + + + + + User Tests + + + + + +

User Tests

+ + + + + + + + + + + + + +
Test TypeTest LengthTest TimeWPMAccuracy
+ + + + \ No newline at end of file diff --git a/public/pages/account/script.js b/public/pages/account/script.js new file mode 100644 index 0000000..7d3f28e --- /dev/null +++ b/public/pages/account/script.js @@ -0,0 +1,22 @@ +async function fetchLeaderboardData() { + const response = await fetch('/api/user/tests/'); + if (!response.ok) { + console.error('Failed to fetch leaderboard data'); + return; + } + const data = await response.json(); + const tableBody = document.getElementById('leaderboardTable').getElementsByTagName('tbody')[0]; + tableBody.innerHTML = ''; // Clear existing rows + data.forEach(item => { + const row = tableBody.insertRow(); + row.insertCell(0).innerText = item.username; + row.insertCell(1).innerText = item.wpm; + row.insertCell(2).innerText = item.accuracy; + row.insertCell(3).innerText = item.test_time; + row.insertCell(4).innerText = item.test_length; + }); +} + +// Ensure this function is called on page load and when the refresh button is clicked. +document.addEventListener('DOMContentLoaded', fetchLeaderboardData); +document.getElementById('refreshButton').addEventListener('click', fetchLeaderboardData); diff --git a/public/pages/account/styles.css b/public/pages/account/styles.css new file mode 100644 index 0000000..b3c9fbb --- /dev/null +++ b/public/pages/account/styles.css @@ -0,0 +1,82 @@ +body { + font-family: Arial, sans-serif; + display: flex; + justify-content: center; + flex-direction: column; + align-items: center; + margin-top: 5%; +} +table { + width: 60%; + border-collapse: collapse; + margin-top: 20px; +} +th, td { + border: 1px solid #ddd; + text-align: left; + padding: 8px; +} +th { + background-color: #f2f2f2; +} +tr:nth-child(even) { + background-color: #f9f9f9; +} +.refresh-button { + cursor: pointer; + border: none; + background-color: transparent; + display: flex; + align-items: center; + color: #4CAF50; +} + +.refresh-button:hover { + transform: rotate(90deg); +} + +/* Dark Mode styles */ +@media (prefers-color-scheme: dark) { + body { + background-color: #121212; + color: #e0e0e0; + } + table { + border-color: #424242; + } + th { + background-color: #333; + } + tr:nth-child(even) { + background-color: #2a2a2a; + } + .refresh-icon { + fill: #90caf9; + } +} + +#navbar { + background-color: #333; /* Navbar background color */ + position: fixed; + top: 0; + width: 100%; + height: 5%; /* Covering the top 20% of the screen */ + z-index: 1000; /* Ensures the navbar is on top of other content */ + display: flex; + justify-content: center; /* Center horizontally */ + align-items: center; /* Center vertically */ +} + +#navbar a { + text-decoration: none; + color: #fff; /* Text color */ + padding: 0.75% 20px; /* Padding for each link */ +} + +#navbar a:hover { + background-color: #555; /* Background color on hover */ +} + +#navbar a.active { + background-color: #555; /* Background color for active link */ +} \ No newline at end of file diff --git a/public/pages/end/index.html b/public/pages/end/index.html new file mode 100644 index 0000000..47dcccf --- /dev/null +++ b/public/pages/end/index.html @@ -0,0 +1,26 @@ + + + + + + User Dashboard + + + + +

User Dashboard

+
+ +
+

Most Recent Test Result

+
+ +
+ + + \ No newline at end of file diff --git a/public/pages/end/script.js b/public/pages/end/script.js new file mode 100644 index 0000000..c4020e7 --- /dev/null +++ b/public/pages/end/script.js @@ -0,0 +1,69 @@ +// Function to fetch the username +async function fetchUsername() { + const userId = localStorage.getItem("userId"); // Replace with the actual user ID + const secret = localStorage.getItem("secret") + + try { + const response = await fetch(`/api/user/test/${userId}/${secret}`); + const userData = await response.json(); + + // Call function to display username + displayUsername(localStorage.getItem("username")); + displayTestResults(userData); + } catch (error) { + console.error('Error fetching username:', error); + } +} + +// Function to display test results in a table +function displayTestResults(testData) { + const testResultsDiv = document.getElementById('testResults'); + + if (!testData) { + testResultsDiv.innerHTML = '

No test data available.

'; + return; + } + + // Construct HTML for the table + const html = ` + + + + + + + + + + + + + + + + + + + +
Test TypeTest LengthTest TimeTest SeedQuote IDWords Per Minute (WPM)Accuracy
${testData.test_type}${testData.test_length}${testData.test_time}${testData.test_seed}${testData.quote_id}${testData.wpm}${testData.accuracy}
+ `; + + // Update the testResultsDiv with the HTML + testResultsDiv.innerHTML = html; +} + +// Function to display username +function displayUsername(username) { + const usernameDiv = document.getElementById('username'); + + if (!username) { + usernameDiv.innerHTML = '

No username available.

'; + return; + } + + // Update the usernameDiv with the username + usernameDiv.innerHTML = `

Welcome, ${username}!

`; +} + +// Call the fetchUsername function when the page loads +window.onload = fetchUsername; \ No newline at end of file diff --git a/public/pages/end/styles.css b/public/pages/end/styles.css new file mode 100644 index 0000000..ea41cb2 --- /dev/null +++ b/public/pages/end/styles.css @@ -0,0 +1,85 @@ +body { + font-family: Arial, sans-serif; + display: flex; + justify-content: center; + flex-direction: column; + align-items: center; + margin-top: 5%; + +} +table { + width: 100%; + height: 100%; + font-size: larger; + border-collapse: collapse; + margin-top: 20px; +} +th, td { + border: 1px solid #ddd; + text-align: center; + padding: 8px; +} +th { + background-color: #f2f2f2; +} +tr:nth-child(even) { + background-color: #f9f9f9; +} +.refresh-button { + cursor: pointer; + border: none; + background-color: transparent; + display: flex; + align-items: center; + color: #4CAF50; +} + +.refresh-button:hover { + transform: rotate(90deg); +} + +/* Dark Mode styles */ +@media (prefers-color-scheme: dark) { + body { + background-color: #121212; + color: #e0e0e0; + } + table { + border-color: #424242; + } + th { + background-color: #333; + } + tr:nth-child(even) { + background-color: #2a2a2a; + } + .refresh-icon { + fill: #90caf9; + } +} + +#navbar { + background-color: #333; /* Navbar background color */ + position: fixed; + top: 0; + width: 100%; + height: 5%; /* Covering the top 20% of the screen */ + z-index: 1000; /* Ensures the navbar is on top of other content */ + display: flex; + justify-content: center; /* Center horizontally */ + align-items: center; /* Center vertically */ +} + +#navbar a { + text-decoration: none; + color: #fff; /* Text color */ + padding: 0.75% 20px; /* Padding for each link */ +} + +#navbar a:hover { + background-color: #555; /* Background color on hover */ +} + +#navbar a.active { + background-color: #555; /* Background color for active link */ +} \ No newline at end of file diff --git a/public/pages/leaderboard/index.html b/public/pages/leaderboard/index.html new file mode 100644 index 0000000..e707802 --- /dev/null +++ b/public/pages/leaderboard/index.html @@ -0,0 +1,35 @@ + + + + + + Leaderboard + + + + +

Leaderboard

+ + + + + + + + + + + + + +
UsernameWPMAccuracy (%)Test Time (s)Test Length (characters)
+ + + + + diff --git a/public/leaderboard/script.js b/public/pages/leaderboard/script.js similarity index 100% rename from public/leaderboard/script.js rename to public/pages/leaderboard/script.js diff --git a/public/pages/leaderboard/styles.css b/public/pages/leaderboard/styles.css new file mode 100644 index 0000000..b3c9fbb --- /dev/null +++ b/public/pages/leaderboard/styles.css @@ -0,0 +1,82 @@ +body { + font-family: Arial, sans-serif; + display: flex; + justify-content: center; + flex-direction: column; + align-items: center; + margin-top: 5%; +} +table { + width: 60%; + border-collapse: collapse; + margin-top: 20px; +} +th, td { + border: 1px solid #ddd; + text-align: left; + padding: 8px; +} +th { + background-color: #f2f2f2; +} +tr:nth-child(even) { + background-color: #f9f9f9; +} +.refresh-button { + cursor: pointer; + border: none; + background-color: transparent; + display: flex; + align-items: center; + color: #4CAF50; +} + +.refresh-button:hover { + transform: rotate(90deg); +} + +/* Dark Mode styles */ +@media (prefers-color-scheme: dark) { + body { + background-color: #121212; + color: #e0e0e0; + } + table { + border-color: #424242; + } + th { + background-color: #333; + } + tr:nth-child(even) { + background-color: #2a2a2a; + } + .refresh-icon { + fill: #90caf9; + } +} + +#navbar { + background-color: #333; /* Navbar background color */ + position: fixed; + top: 0; + width: 100%; + height: 5%; /* Covering the top 20% of the screen */ + z-index: 1000; /* Ensures the navbar is on top of other content */ + display: flex; + justify-content: center; /* Center horizontally */ + align-items: center; /* Center vertically */ +} + +#navbar a { + text-decoration: none; + color: #fff; /* Text color */ + padding: 0.75% 20px; /* Padding for each link */ +} + +#navbar a:hover { + background-color: #555; /* Background color on hover */ +} + +#navbar a.active { + background-color: #555; /* Background color for active link */ +} \ No newline at end of file diff --git a/public/leaderboard/index.html b/public/pages/test-settings/index.html similarity index 71% rename from public/leaderboard/index.html rename to public/pages/test-settings/index.html index 78ff213..7f44ded 100644 --- a/public/leaderboard/index.html +++ b/public/pages/test-settings/index.html @@ -7,7 +7,13 @@ -

Leaderboard

+ +

Test Settings

diff --git a/public/pages/test-settings/script.js b/public/pages/test-settings/script.js new file mode 100644 index 0000000..0b78b2c --- /dev/null +++ b/public/pages/test-settings/script.js @@ -0,0 +1,22 @@ +async function fetchLeaderboardData() { + const response = await fetch('/api/leaderboard'); + if (!response.ok) { + console.error('Failed to fetch leaderboard data'); + return; + } + const data = await response.json(); + const tableBody = document.getElementById('leaderboardTable').getElementsByTagName('tbody')[0]; + tableBody.innerHTML = ''; // Clear existing rows + data.forEach(item => { + const row = tableBody.insertRow(); + row.insertCell(0).innerText = item.username; + row.insertCell(1).innerText = item.wpm; + row.insertCell(2).innerText = item.accuracy; + row.insertCell(3).innerText = item.test_time; + row.insertCell(4).innerText = item.test_length; + }); +} + +// Ensure this function is called on page load and when the refresh button is clicked. +document.addEventListener('DOMContentLoaded', fetchLeaderboardData); +document.getElementById('refreshButton').addEventListener('click', fetchLeaderboardData); diff --git a/public/leaderboard/styles.css b/public/pages/test-settings/styles.css similarity index 59% rename from public/leaderboard/styles.css rename to public/pages/test-settings/styles.css index 361e575..78918f3 100644 --- a/public/leaderboard/styles.css +++ b/public/pages/test-settings/styles.css @@ -54,3 +54,29 @@ tr:nth-child(even) { fill: #90caf9; } } + +#navbar { + background-color: #333; /* Navbar background color */ + position: fixed; + top: 0; + width: 100%; + height: 5%; /* Covering the top 20% of the screen */ + z-index: 1000; /* Ensures the navbar is on top of other content */ + display: flex; + justify-content: center; /* Center horizontally */ + align-items: center; /* Center vertically */ +} + +#navbar a { + text-decoration: none; + color: #fff; /* Text color */ + padding: 0.75% 20px; /* Padding for each link */ +} + +#navbar a:hover { + background-color: #555; /* Background color on hover */ +} + +#navbar a.active { + background-color: #555; /* Background color for active link */ +} \ No newline at end of file diff --git a/public/assets/favicon/android-chrome-192x192.png b/public/pages/test/assets/favicon/android-chrome-192x192.png similarity index 100% rename from public/assets/favicon/android-chrome-192x192.png rename to public/pages/test/assets/favicon/android-chrome-192x192.png diff --git a/public/assets/favicon/android-chrome-512x512.png b/public/pages/test/assets/favicon/android-chrome-512x512.png similarity index 100% rename from public/assets/favicon/android-chrome-512x512.png rename to public/pages/test/assets/favicon/android-chrome-512x512.png diff --git a/public/assets/favicon/apple-touch-icon.png b/public/pages/test/assets/favicon/apple-touch-icon.png similarity index 100% rename from public/assets/favicon/apple-touch-icon.png rename to public/pages/test/assets/favicon/apple-touch-icon.png diff --git a/public/assets/favicon/favicon-16x16.png b/public/pages/test/assets/favicon/favicon-16x16.png similarity index 100% rename from public/assets/favicon/favicon-16x16.png rename to public/pages/test/assets/favicon/favicon-16x16.png diff --git a/public/assets/favicon/favicon-32x32.png b/public/pages/test/assets/favicon/favicon-32x32.png similarity index 100% rename from public/assets/favicon/favicon-32x32.png rename to public/pages/test/assets/favicon/favicon-32x32.png diff --git a/public/assets/favicon/favicon.ico b/public/pages/test/assets/favicon/favicon.ico similarity index 100% rename from public/assets/favicon/favicon.ico rename to public/pages/test/assets/favicon/favicon.ico diff --git a/public/assets/favicon/site.webmanifest b/public/pages/test/assets/favicon/site.webmanifest similarity index 100% rename from public/assets/favicon/site.webmanifest rename to public/pages/test/assets/favicon/site.webmanifest diff --git a/public/assets/fonts/RobotoMono-Medium.ttf b/public/pages/test/assets/fonts/RobotoMono-Medium.ttf similarity index 100% rename from public/assets/fonts/RobotoMono-Medium.ttf rename to public/pages/test/assets/fonts/RobotoMono-Medium.ttf diff --git a/public/assets/icons/account_circle_black.svg b/public/pages/test/assets/icons/account_circle_black.svg similarity index 100% rename from public/assets/icons/account_circle_black.svg rename to public/pages/test/assets/icons/account_circle_black.svg diff --git a/public/assets/icons/account_circle_white.svg b/public/pages/test/assets/icons/account_circle_white.svg similarity index 100% rename from public/assets/icons/account_circle_white.svg rename to public/pages/test/assets/icons/account_circle_white.svg diff --git a/public/components/button.js b/public/pages/test/components/button.js similarity index 100% rename from public/components/button.js rename to public/pages/test/components/button.js diff --git a/public/components/canvas.js b/public/pages/test/components/canvas.js similarity index 74% rename from public/components/canvas.js rename to public/pages/test/components/canvas.js index 7aa8eac..722eda6 100755 --- a/public/components/canvas.js +++ b/public/pages/test/components/canvas.js @@ -17,12 +17,12 @@ class Canvas { center() { - this.canvas.position(this.x, this.y); + this.canvas.position(this.x, this.y + windowHeight / 100 * 5); } resize() { - this.canvas.resize(windowWidth, windowHeight); + this.canvas.resize(windowWidth, windowHeight - windowHeight / 100 * 5); } disable() { diff --git a/public/components/menu.js b/public/pages/test/components/menu.js similarity index 100% rename from public/components/menu.js rename to public/pages/test/components/menu.js diff --git a/public/components/textbox.js b/public/pages/test/components/textbox.js similarity index 100% rename from public/components/textbox.js rename to public/pages/test/components/textbox.js diff --git a/public/components/timemenu.js b/public/pages/test/components/timemenu.js similarity index 100% rename from public/components/timemenu.js rename to public/pages/test/components/timemenu.js diff --git a/public/components/timer.js b/public/pages/test/components/timer.js similarity index 96% rename from public/components/timer.js rename to public/pages/test/components/timer.js index 950be0a..f99a567 100755 --- a/public/components/timer.js +++ b/public/pages/test/components/timer.js @@ -48,6 +48,7 @@ class Timer { this.timeElapsed = 0; this.ended; this.hasStarted = false; + this.hasEnded = false; } getX() { @@ -163,7 +164,7 @@ class Timer { */ tick() { this.timeElapsed = (millis() - this.startTime) / 1000; - if (this.timeElapsed >= this.time) { + if (this.timeElapsed >= this.time && !this.testEnded) { this.end(); }; } @@ -172,14 +173,13 @@ class Timer { * this function is called at the end of the timer */ end() { + api.createTest() this.visible = false; - api.validateTest(); this.timeElapsed = 0; this.time = 0; - api.getTest(); // Then this function will call all other functions necessary to complete the test // this will likely including changing the screen and interacting with the api - screenManager.setScreen(new EndScreen()); + this.hasEnded = true } /** @@ -188,6 +188,7 @@ class Timer { draw() { // if the time shouldn't be rendered it quickly exits out of this method if (!this.visible) return; + if (this.hasEnded) return; textAlign(LEFT); // adds a border for the bar if one is needed diff --git a/public/components/user.js b/public/pages/test/components/user.js similarity index 100% rename from public/components/user.js rename to public/pages/test/components/user.js diff --git a/public/pages/test/index.css b/public/pages/test/index.css new file mode 100755 index 0000000..9a48dc5 --- /dev/null +++ b/public/pages/test/index.css @@ -0,0 +1,74 @@ +/* +Index.css +Description: This file is the stylesheet for the html that the + user will see if they do not have javascript enabled. +Author: Arlo Filley +*/ + +:root { + --background-color: #000000; + --text-color: yellow; + --font: Verdana, Geneva, Tahoma, sans-serif; +} + +body { + background-color: var(--background-color); + position: absolute; + margin: 0; + font-family: Arial, sans-serif; +} + +noscript { + display: block; + text-align: center; + + font-family: var(--font); + color: var(--text-color); + font-size: large; +} + +#navbar { + background-color: #333; /* Navbar background color */ + position: fixed; + top: 0; + width: 100%; + height: 5%; /* Covering the top 20% of the screen */ + z-index: 1000; /* Ensures the navbar is on top of other content */ + display: flex; + justify-content: center; /* Center horizontally */ + align-items: center; /* Center vertically */ +} + +#navbar a { + text-decoration: none; + color: #fff; /* Text color */ + padding: 0.7% 20px; /* Padding for each link */ +} + +#navbar a:hover { + background-color: #555; /* Background color on hover */ +} + +#navbar a.active { + background-color: #555; /* Background color for active link */ +} + +/* Dark Mode styles */ +@media (prefers-color-scheme: dark) { + body { + background-color: #121212; + color: #e0e0e0; + } + table { + border-color: #424242; + } + th { + background-color: #333; + } + tr:nth-child(even) { + background-color: #2a2a2a; + } + .refresh-icon { + fill: #90caf9; + } +} \ No newline at end of file diff --git a/public/index.html b/public/pages/test/index.html similarity index 70% rename from public/index.html rename to public/pages/test/index.html index 5933f71..547d035 100755 --- a/public/index.html +++ b/public/pages/test/index.html @@ -4,7 +4,7 @@ - TypeFast + Typing - Test @@ -15,6 +15,13 @@ + + @@ -31,19 +38,11 @@ - - - - - - - - - - + + diff --git a/public/pages/test/index.js b/public/pages/test/index.js new file mode 100644 index 0000000..19384fb --- /dev/null +++ b/public/pages/test/index.js @@ -0,0 +1,72 @@ +/** + * @file This file is the root of the website. + * @author Arlo Filley + */ + +/** + * Loads any assets before the setup function. + * This allows p5.js to access these assets including: sprites, + * fonts, etc. + * @returns {void} + */ +function preload() { + roboto = loadFont('./assets/fonts/RobotoMono-Medium.ttf'); +} + +/** + * Defines variables and sets up the p5.js canvas + * ready to be drawn with using the draw() function. + * @returns {void} + */ +async function setup() { + canvas = new Canvas(); + canvas.resize(); + canvas.center(); + + frameRate(60); + + api = new API(); + screenManager = new ScreenManager(); + /** + * @type {User} + */ + user = new User(); + await api.getTest(); + console.log(user.nextTest); + screenManager.setScreen(new TestScreen()); + + api.login(null, null, true); + textFont(roboto); +} + + +/** + * Called once per frame. Draws all other elements onto the canvas. + * Mostly will just call the screenManager.draw() method to make + * sure that the correct screen is being drawn. + * @returns {void} + */ +function draw() { + background(user.colorScheme.background); + screenManager.draw(); +} + +/** + * Called whenever a key is pressed. The variable key contains the + * key that the user last pressed. + * @returns {void} + */ +function keyPressed() { + screenManager.letterTyped(key); +} + + +/** + * Called whenever the user resizes the window. Uses methods from the canvas wrapper class + * to resize and center the canvas such that it displays correctly. + * @returns {void} + */ +function windowResized() { + canvas.resize(); + canvas.center(); +} \ No newline at end of file diff --git a/public/screens/screenmanager.js b/public/pages/test/screens/screenmanager.js similarity index 100% rename from public/screens/screenmanager.js rename to public/pages/test/screens/screenmanager.js diff --git a/public/screens/testscreen.js b/public/pages/test/screens/testscreen.js similarity index 84% rename from public/screens/testscreen.js rename to public/pages/test/screens/testscreen.js index a96e7e7..a3a0c1e 100755 --- a/public/screens/testscreen.js +++ b/public/pages/test/screens/testscreen.js @@ -31,15 +31,21 @@ class TestScreen { } this.stopButton.draw(); if (this.stopButton.isPressed()) { - screenManager.setScreen(new StartScreen()) + window.location.href = "/typing/pages/leaderboard/index.html" + } + + if (this.timer.hasEnded) { + this.stopButton.y = 0 + text("Test Has Ended", 100, 100); } } letterTyped(key) { + if (this.timer.hasEnded) return; this.textbox.letterTyped(key); if (!this.timerStarted) { this.timer.start(); this.timerStarted = true; - } + } } } \ No newline at end of file diff --git a/public/screens/accountScreen.js b/public/screens/accountScreen.js deleted file mode 100755 index bf793dc..0000000 --- a/public/screens/accountScreen.js +++ /dev/null @@ -1,115 +0,0 @@ -/** - * @file This file provides a screen for the user to login through - * @author Arlo Filley - * - * TODO: - * - move into an seperated account page with signup and logout - * - make passwords not display plain text - */ - -/** - * this class displays a number of textboxes that allows the user to input a - * username and password. Then find out the user_id of that account through the - * necessary api routes. - */ -class AccountScreen { - constructor() { - this.textboxes = [ - new Textbox( - 120, 350, 500, 100, 0, true, "#000", false, - "#000", "#000", true - ), - - new Textbox( - 120, 500, 500, 100, 0, true, "#000", false, - "#000", "#000", false, false, true - ) - ] - - this.buttons = [ - new Button(windowWidth / 2 - 400, 300, 500, 100, "", false, true, "#000", "#000", "#fff", "#000", "#000", "#fff"), - new Button(windowWidth / 2 - 400, 450, 500, 100, "", false, true, "#000", "#000", "#fff", "#000", "#000", "#fff"), - new Button(windowWidth / 2 + 200, 300, 100, 50, "Login"), - new Button(windowWidth / 2 + 200, 400, 100, 50, "Sign up"), - new Button(windowWidth / 2 + 200, 500, 100, 50, "Logout"), - ] - - this.menu = new Menu(); - - // keeps track of which textbox the user last clicked on - this.activeTextBox = 0 - } - - /** - * Draws the SignUpScreen class with all - * appropriate elements - */ - draw() { - textSize(100); - fill(user.colorScheme.text); - text("Account", 0, 100, windowWidth, 110); - for (let i = 0; i < this.buttons.length; i++) { - this.buttons[i].draw(); - } - - for (let i = 0; i < this.textboxes.length; i++) { - this.textboxes[i].draw(); - } - - textSize(30); - text("Username", windowWidth / 2 - 400, 275); - text("Password", windowWidth / 2 - 400, 425); - - if (this.buttons[0].isPressed()) { - this.textboxes[this.activeTextBox].line = false; - this.activeTextBox=0; - this.textboxes[this.activeTextBox].line = true; - } else if (this.buttons[1].isPressed()) { - this.textboxes[this.activeTextBox].line = false; - this.activeTextBox=1; - this.textboxes[this.activeTextBox].line = true; - } else if (this.buttons[2].isPressed()) { - api.login( - this.textboxes[0].getWords(), - this.textboxes[1].getWords() - ) - screenManager.setScreen(new StartScreen()); - } else if (this.buttons[3].isPressed()) { - api.createUser( - this.textboxes[0].getWords(), - this.textboxes[1].getWords() - ) - screenManager.setScreen(new StartScreen()); - } else if (this.buttons[4].isPressed()) { - api.logout(); - screenManager.setScreen(new StartScreen()); - } - this.menu.draw(); - - fill(user.colorScheme.text); - } - - /** - * - * @param {key} key - */ - letterTyped(key) { - if (key === "Tab" && this.activeTextBox === 0) { - this.textboxes[this.activeTextBox].line = false; - this.activeTextBox=1; - this.textboxes[this.activeTextBox].line = true; - } else if (key === "Tab" && this.activeTextBox === 1) { - this.textboxes[this.activeTextBox].line = false; - this.activeTextBox=0; - this.textboxes[this.activeTextBox].line = true; - } else if (key === "Enter") { - api.login( - this.textboxes[0].getWords(), - this.textboxes[1].getWords() - ) - screenManager.setScreen(new StartScreen()); - } else { - this.textboxes[this.activeTextBox].letterTyped(key); - } - } -} \ No newline at end of file diff --git a/public/screens/endscreen.js b/public/screens/endscreen.js deleted file mode 100755 index d99b244..0000000 --- a/public/screens/endscreen.js +++ /dev/null @@ -1,36 +0,0 @@ -/** - * @file This file provides a screen class that can be displayed at the end of a test - * @author Arlo Filley - * - * TODO: - * - provide the user with the data of the test that they have just - * completed, such as their wpm, accuracy, etc. - */ - -/** - * This class is for a screen that is displayed at the end of a test, - * currently it just tells the user to press start to enter another test - */ -class EndScreen { - constructor() { - this.menu = new Menu(); - } - - draw() { - textSize(100); - textAlign(CENTER, CENTER); - fill(user.colorScheme.text); - text("Test Complete", 0, 0, windowWidth - 100, windowHeight / 6); - - textSize(30); - text(`${user.lastTest.wpm} words per minute`, windowWidth / 2, 200); - text(`${user.lastTest.accuracy}% accuracy`, windowWidth / 2, 240); - text(`${user.lastTest.test_length} characters typed`, windowWidth / 2, 280); - text(`${user.lastTest.test_time}s`, windowWidth / 2, 320); - this.menu.draw(); - } - - letterTyped(key) { - if (key === "Enter") screenManager.setScreen(new TestScreen()); - } -} \ No newline at end of file diff --git a/public/screens/leaderboardscreen.js b/public/screens/leaderboardscreen.js deleted file mode 100755 index ef8b57a..0000000 --- a/public/screens/leaderboardscreen.js +++ /dev/null @@ -1,101 +0,0 @@ -/** - * @file This file provides a leaderboard for the user to compare times. - * @author Arlo Filley - * - * TODO: - * - implement a way for the user to scroll down the tests. - * - display more tests on the screen at once, up to 15 - * - store the leaderboard in localstorage as a cache for the most recent results - */ - -/** - * this class is a screen which shows the current leaderboard from the - * results gotten via the api. - */ -class LeaderboardScreen { - constructor() { - this.menu = new Menu(); - api.getLeaderBoard(); - this.testButtons; - // this.buttons = [ - // new Button(1150, 270, 240, 120, "up"), - // new Button(1150, 390, 240, 120, "down"), - // ] - this.offset = 0; - this.scroll_bar_button = new Button(1200, 200, 20, 20, "") - } - - draw() { - textSize(100); - textAlign(CENTER, CENTER); - fill(user.colorScheme.text); - text("Leaderboard", 0, 100, windowWidth, 120); - this.menu.draw(); - - textSize(20); - fill(user.colorScheme.text); - if (user.leaderboard != undefined) { - if (this.testButtons === undefined) { - this.createTestButtons(); - } - } - - fill(user.colorScheme.testBad); - rect(1200, 270, 20, 420); - - fill(user.colorScheme.testGood); - rect(1200, 270, 20, 420 / user.leaderboard.length * (this.offset + 1)); - this.scroll_bar_button.height = (user.leaderboard.length) - - if (this.scroll_bar_button.isPressed()) { - this.scroll_bar_button.y = mouseY - this.scroll_bar_button.height / 2; - } - - // the furthest up the scrollbar can go is the top - if (this.scroll_bar_button.y < 270) { - this.scroll_bar_button.y = 270; - } - - if (this.scroll_bar_button.y > 690 - this.scroll_bar_button.height) { - this.scroll_bar_button.y = 690 - this.scroll_bar_button.height; - } - this.scroll_bar_button.draw(); - - if (this.testButtons !== undefined && this.testButtons.length > 1) { - for (let i = 0; i < this.testButtons.length; i++) { - this.testButtons[i][0].draw() - this.testButtons[i][1].draw() - this.testButtons[i][2].draw() - } - - this.offset = Number(( - // number of pixels from top of screen / total range of options, put to a whole integer to produce the correct offset - (this.scroll_bar_button.y - 270) / ((420 - this.scroll_bar_button.height) / (user.leaderboard.length - 13)) - ).toFixed(0)); - - this.createTestButtons(this.offset) - } else { - fill(user.colorScheme.text); - text("Looks Like There Isn't A Leaderboard", windowWidth / 2, 300); - } - - fill(user.colorScheme.text); - } - - createTestButtons(offset = 0) { - this.testButtons = [[ - new Button(400, 270, 100, 30, "ranking"), // test # button - new Button(500, 270, 400, 30, "username"), // wpm button - new Button(900, 270, 240, 30, "words per minute"), // accuracy button - ]]; - let j = 300; - for (let i = 0 + offset; i < user.leaderboard.length && i <= 12+offset; i++) { - this.testButtons.push([ - new Button(400, j, 100, 30, `${i+1}`, true, true, "#000", "#000", "#fff"), // test # button - new Button(500, j, 400, 30, `${user.leaderboard[i].username}`, true, true, "#000", "#000", "#fff"), // accuracy button - new Button(900, j, 240, 30, `${user.leaderboard[i].wpm}`, true, true, "#000", "#000", "#fff"), // wpm button - ]) - j+=30; - } - } -} \ No newline at end of file diff --git a/public/screens/loginscreen.js b/public/screens/loginscreen.js deleted file mode 100755 index 20802f0..0000000 --- a/public/screens/loginscreen.js +++ /dev/null @@ -1,108 +0,0 @@ -/** - * @file This file provides a screen for the user to login through - * @author Arlo Filley - * - * TODO: - * - move into an seperated account page with signup and logout - * - make passwords not display plain text - */ - -/** - * this class displays a number of textboxes that allows the user to input a - * username and password. Then find out the user_id of that account through the - * necessary api routes. - */ -class LoginScreen { - constructor() { - this.textboxes = [ - new Textbox( - 120, 250, 500, 100, 0, true, user.colorScheme.text, false, - "#000", "#000", true, false - ), - - new Textbox( - 120, 400, 500, 100, 0, true, user.colorScheme.text, false, - "000", "#000", false, false - ) - ] - - this.buttons = [ - new Button( - 100, 200, 500, 100, 0, true, user.colorScheme.buttonBG, false, - "#000", "#fff", "" - ), - - new Button( - 100, 350, 500, 100, 0, true, "#000", false, - "#000", "#fff", "" - ), - - new Button( - 700, 300, 100, 50, 0, true, "#000", false, - "#000", "#00ff00", "Login" - ), - ] - - this.menu = new Menu(); - - // keeps track of which textbox the user last clicked on - this.activeTextBox = 0 - } - - /** - * Draws the SignUpScreen class with all - * appropriate elements - */ - draw() { - fill(user.colorScheme.text); - text("Username", 110, 175); - text("Password", 110, 325); - - for (let i = 0; i < this.buttons.length; i++) { - this.buttons[i].draw(); - } - - for (let i = 0; i < this.textboxes.length; i++) { - this.textboxes[i].draw(); - } - - if (this.buttons[0].isPressed()) { - this.textboxes[this.activeTextBox].line = false; - this.activeTextBox=0; - this.textboxes[this.activeTextBox].line = true; - } else if (this.buttons[1].isPressed()) { - this.textboxes[this.activeTextBox].line = false; - this.activeTextBox=1; - this.textboxes[this.activeTextBox].line = true; - } else if (this.buttons[2].isPressed()) { - api.login(this.textboxes[0].getWords(), this.textboxes[1].getWords()) - screenManager.setScreen(new StartScreen()); - } - - this.menu.draw(); - } - - /** - * - * @param {key} key - */ - letterTyped(key) { - if (key === "Tab" && this.activeTextBox === 0) { - this.textboxes[this.activeTextBox].line = false; - this.activeTextBox = 1; - this.textboxes[this.activeTextBox].line = true; - } else if (key === "Tab" && this.activeTextBox === 1) { - this.textboxes[this.activeTextBox].line = false; - this.activeTextBox = 0; - this.textboxes[this.activeTextBox].line = true; - } else if (key === "Enter") { - api.login( - this.textboxes[0].getWords(), - this.textboxes[1].getWords() - ) - screenManager.setScreen(new StartScreen()); - } else { - this.textboxes[this.activeTextBox].letterTyped(key); - } - } -} \ No newline at end of file diff --git a/public/screens/profilescreen.js b/public/screens/profilescreen.js deleted file mode 100755 index 6837d02..0000000 --- a/public/screens/profilescreen.js +++ /dev/null @@ -1,111 +0,0 @@ -/** - * @file This file provides the user with their profilescreen, where they can see their own tests - * @author Arlo Filley - * - * TODO: - * - change button name - * - provide filters for tests - * - implement a way to scroll through tests - * - create a way to have personal bests and track them - * - store tests in localstorage. - * - show user tests even if they are not logged in - */ - -/** - * This class displays all of the test data for a given user - */ -class ProfileScreen { - constructor() { - this.menu = new Menu(); - api.getUserTests(); - this.testButtons; - // this.buttons = [ - // new Button(950, 270, 240, 120, "up"), - // new Button(950, 390, 240, 120, "down"), - // ] - this.offset = 0; - this.scroll_bar_button = new Button(1200, 200, 20, 20, "") - } - - draw() { - textSize(100); - textAlign(CENTER, CENTER); - fill(user.colorScheme.text); - text("Profile", 0, 100, windowWidth, 120); - - this.menu.draw(); - - textSize(20); - fill(user.colorScheme.text); - if (user.tests != undefined) { - if (this.testButtons === undefined) { - this.createTestButtons(); - } - } - - if (this.testButtons !== undefined && this.testButtons.length > 1) { - for (let i = 0; i < this.testButtons.length; i++) { - this.testButtons[i][0].draw() - this.testButtons[i][1].draw() - this.testButtons[i][2].draw() - this.testButtons[i][3].draw() - } - } else { - fill(user.colorScheme.text); - text("Looks Like You Don't have any tests :(", windowWidth / 2, 300); - } - - fill(user.colorScheme.text); - - if (user.tests === undefined) { - return; - } - - fill(user.colorScheme.testBad); - rect(1200, 270, 20, 420); - - fill(user.colorScheme.testGood); - rect(1200, 270, 20, 420 / user.tests.length * (this.offset + 1)); - this.scroll_bar_button.height = (user.tests.length) - - if (this.scroll_bar_button.isPressed()) { - this.scroll_bar_button.y = mouseY - this.scroll_bar_button.height / 2; - } - - - // the furthest up the scrollbar can go is the top - if (this.scroll_bar_button.y < 270) { - this.scroll_bar_button.y = 270; - } - - if (this.scroll_bar_button.y > 690 - this.scroll_bar_button.height) { - this.scroll_bar_button.y = 690 - this.scroll_bar_button.height; - } - this.scroll_bar_button.draw(); - - this.offset = Number(( - // number of pixels from top of screen / total range of options, put to a whole integer to produce the correct offset - (this.scroll_bar_button.y - 270) / ((420 - this.scroll_bar_button.height) / (user.tests.length - 13)) - ).toFixed(0)); - this.createTestButtons(this.offset); - } - - createTestButtons(offset = 0) { - this.testButtons = [[ - new Button(600, 270, 100, 30, "test #"), // test # button - new Button(700, 270, 100, 30, "wpm"), // wpm button - new Button(800, 270, 100, 30, "accuracy"), // accuracy button - new Button(900, 270, 240, 30, "characters typed") - ]]; - let j = 300; - for (let i = user.tests.length-1-offset; i >= user.tests.length-13-offset && i >= 0; i--) { - this.testButtons.push([ - new Button(600, j, 100, 30, `${i+1}`, true, true , "#000", "#000", "#fff"), // test # button - new Button(700, j, 100, 30, `${user.tests[i].wpm}`, true, true , "#000", "#000", "#fff"), // accuracy button - new Button(800, j, 100, 30, `${user.tests[i].accuracy}`, true, true , "#000", "#000", "#fff"), // wpm button - new Button(900, j, 240, 30, `${user.tests[i].test_length}`, true, true , "#000", "#000", "#fff") - ]) - j+=30; - } - } -} \ No newline at end of file diff --git a/public/screens/settingsScreen.js b/public/screens/settingsScreen.js deleted file mode 100755 index 1c44857..0000000 --- a/public/screens/settingsScreen.js +++ /dev/null @@ -1,29 +0,0 @@ -/** - * @file This file provides a screen where the user can edit the settings of their tests - * @author Arlo Filley - */ - -/** - * This class provides all of the necessary settings for the user to be able to edit test settings - */ -class SettingsScreen { - constructor() { - this.menu = new Menu(); - this.timeMenu = new TimeMenu(); - } - - draw() { - textAlign(CENTER, CENTER); - - textSize(100); - fill(user.colorScheme.text); - text("Test Settings", 0, 100, windowWidth, 110); - - this.menu.draw(); - - fill(user.colorScheme.text); - text("Test Duration", windowWidth / 2 - 250, 265) - this.timeMenu.draw(); - fill("#000"); - } -} \ No newline at end of file diff --git a/public/screens/signUpScreen.js b/public/screens/signUpScreen.js deleted file mode 100755 index 95f2691..0000000 --- a/public/screens/signUpScreen.js +++ /dev/null @@ -1,109 +0,0 @@ -/** - * @file This file provides a way for the user to sign up for an account - * @author Arlo Filley - * - * TODO: - * - move into an seperated account page with signup and logout - * - make passwords not display plain text - */ - -/** - * This class provides the textboxes and methods necessary for a user - * to sign up for a new account, which it should then log them into - */ -class SignUpScreen { - constructor() { - this.textboxes = [ - new Textbox( - 120, 250, 500, 100,0, true, "#000", false, - "#000", "#000", true - ), - - new Textbox( - 120, 400, 500, 100, 0, true, "#000", false, - "000", "#000", false - ) - ] - - this.buttons = [ - new Button( - 100, 200, 500, 100, 0, true, "#000", false, - "#000", "#fff", "" - ), - - new Button( - 100, 350, 500, 100, 0, true, "#000", false, - "#000", "#fff", "" - ), - - new Button( - 700, 300, 100, 50, 0, true, "#000", false, - "#000", "#00ff00", "Sign Up" - ), - ] - - this.menu = new Menu(); - - this.activeTextBox = 0 - // keeps track of which textbox the user last clicked on - } - - /** - * Draws the SignUpScreen class with all - * appropriate elements - */ - draw() { - for (let i = 0; i < this.buttons.length; i++) { - this.buttons[i].draw(); - } - - for (let i = 0; i < this.textboxes.length; i++) { - this.textboxes[i].draw(); - } - - fill(user.colorScheme.text); - text("Username", 110, 175); - text("Password", 110, 325); - - if (this.buttons[0].isPressed()) { - this.textboxes[this.activeTextBox].line = false; - this.activeTextBox=0; - this.textboxes[this.activeTextBox].line = true; - } else if (this.buttons[1].isPressed()) { - this.textboxes[this.activeTextBox].line = false; - this.activeTextBox=1; - this.textboxes[this.activeTextBox].line = true; - } else if (this.buttons[2].isPressed()) { - api.createUser( - this.textboxes[0].getWords(), - this.textboxes[1].getWords() - ) - screenManager.setScreen(new StartScreen()); - } - this.menu.draw(); - } - - /** - * - * @param {key} key - */ - letterTyped(key) { - if (key === "Tab" && this.activeTextBox === 0) { - this.textboxes[this.activeTextBox].line = false; - this.activeTextBox=1; - this.textboxes[this.activeTextBox].line = true; - } else if (key === "Tab" && this.activeTextBox === 1) { - this.textboxes[this.activeTextBox].line = false; - this.activeTextBox=0; - this.textboxes[this.activeTextBox].line = true; - } else if (key === "Enter") { - api.createUser( - this.textboxes[0].getWords(), - this.textboxes[1].getWords() - ) - screenManager.setScreen(new StartScreen()); - } else { - this.textboxes[this.activeTextBox].letterTyped(key); - } - } -} \ No newline at end of file diff --git a/public/screens/startscreen.js b/public/screens/startscreen.js deleted file mode 100755 index 6d5aa41..0000000 --- a/public/screens/startscreen.js +++ /dev/null @@ -1,31 +0,0 @@ -/** - * @file This file is the base screen when the user visits the site - * @author Arlo Filley - */ - -/** - * This screen class is the base screen. It provides the user with basic instructions - * and a set of menus to navigate the site - */ -class StartScreen { - constructor() { - this.menu = new Menu(); - } - - draw() { - textSize(100); - textAlign(CENTER, CENTER); - fill(user.colorScheme.text); - text("Press enter to start test", 0, 0, windowWidth, windowHeight); - - this.menu.draw(); - - fill(user.colorScheme.text); - } - - letterTyped(key) { - if (key === "Enter") { - screenManager.setScreen(new TestScreen()); - } - } -} \ No newline at end of file diff --git a/src/api/mod.rs b/src/api/mod.rs index 0bda601..ba7d887 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -2,3 +2,4 @@ pub mod leaderboard; pub mod sql; pub mod test; pub mod user; +pub mod ping; \ No newline at end of file diff --git a/src/api/ping.rs b/src/api/ping.rs new file mode 100644 index 0000000..db8fcb8 --- /dev/null +++ b/src/api/ping.rs @@ -0,0 +1,17 @@ +/// # Ping Endpoint +/// ## GET `/api/ping` +/// Shows whether the api is up and running. Accessible from http://url/api/ping +/// ## Responses +/// - `200 OK`: The api is up and running +/// ## Example Request +/// ```bash +/// curl -X GET "https://url/api/ping" +/// ``` +/// ## Example Response +/// ``` +/// "Hello World! I'm A rocket Webserver" +/// ``` +#[get("/ping")] +pub fn ping() -> &'static str { + "Hello World! I'm A rocket Webserver" +} \ No newline at end of file diff --git a/src/api/sql.rs b/src/api/sql.rs index e1edf3a..4c076ef 100644 --- a/src/api/sql.rs +++ b/src/api/sql.rs @@ -13,7 +13,7 @@ use rocket::serde::{json::Json, Serialize}; use sqlx::sqlite::{SqlitePool, SqlitePoolOptions}; -use crate::api::test::PostTest; +use crate::api::user::test::PostTest; /// Contains the database connection pool pub struct Database(SqlitePool); @@ -141,25 +141,17 @@ 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, sqlx::Error> { - let tests = sqlx::query!( - " + 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=? AND users.secret=?", - user_id, - secret + user_id, secret ) .fetch_all(&self.0) .await?; - println!("{}", tests.len()); - let user_tests = tests .iter() .map(|test| Test { @@ -176,12 +168,35 @@ impl Database { Ok(user_tests) } + pub async fn get_user_test(&self, user_id: u32) -> Result { + let test = 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=? + ORDER BY test_id DESC + LIMIT 1;", + user_id + ) + .fetch_one(&self.0) + .await?; + + let user_tests = 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, + }; + + 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> { + pub async fn get_leaderboard(&self, _user_id: u32) -> Result, sqlx::Error> { let tests = sqlx::query!( "SELECT users.username, tests.wpm, tests.accuracy, tests.test_time, tests.test_length FROM tests diff --git a/src/api/test.rs b/src/api/test.rs index 108c281..e379a22 100644 --- a/src/api/test.rs +++ b/src/api/test.rs @@ -1,59 +1,40 @@ -use crate::api::sql::Database; use rand::{rngs::ThreadRng, Rng}; -use rocket::{ - serde::{json::Json, Deserialize}, - State, -}; +use rocket::serde::json::Json; +use rocket::http::Status; use std::{fs, vec}; -/// the datascructure that the webserver will recieve -/// when a post is made to the http://url/api/post_test route -#[derive(Deserialize)] -#[serde(crate = "rocket::serde")] -pub struct PostTest<'r> { - pub test_type: &'r str, - pub test_length: u32, - pub test_time: u32, - pub test_seed: i64, - pub quote_id: i32, - pub wpm: u8, - pub accuracy: u8, - pub user_id: u32, - pub secret: &'r str, -} - -/// Api Route that accepts test data and posts it to the database -/// Acessible from http://url/api/post_test -#[post("/post_test", data = "")] -pub async fn create_test(test: Json>, database: &State) { - 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}"); - } - } -} - /// Returns an array of words as Json /// Accessible from http://url/api/get_test -#[get("/new_test")] -pub fn new_test() -> Json> { +#[get("/test")] +pub fn generate_test() -> Result>, Status> { let mut word_vec: Vec<&str> = vec![]; - let words: String = fs::read_to_string("wordlist.txt").unwrap(); - for word in words.split('\n') { + + // Read file + let words = match fs::read_to_string("wordlist.txt") { + Err(_) => { + println!(" >> wordlist.txt could not be found"); + return Err(Status::NotFound) + }, + Ok(words) => words, + }; + + // Populate word_vec + for word in words.lines() { word_vec.push(word); } + // Check if word_vec is empty + if word_vec.is_empty() { + return Err(Status::InternalServerError); + } + let mut return_list: Vec = vec![]; let mut rng: ThreadRng = rand::thread_rng(); for _ in 0..100 { - let word = rng.gen_range(0..999); - return_list.push(word_vec[word].to_string()) + let word_index = rng.gen_range(0..word_vec.len()); + return_list.push(word_vec[word_index].to_string()); } - Json(return_list.clone()) + Ok(Json(return_list)) } diff --git a/src/api/user.rs b/src/api/user.rs deleted file mode 100644 index 637d4e8..0000000 --- a/src/api/user.rs +++ /dev/null @@ -1,148 +0,0 @@ -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 = "")] -pub async fn sign_up(user: Json>, database: &State) { - 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// -/// -/// # 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//")] -pub async fn get_tests(user_id: u32, secret: &str, database: &State) -> Result>, 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//")] -pub async fn login( - username: &str, - password: &str, - database: &State, -) -> Json> { - 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, -} diff --git a/src/api/user/create.rs b/src/api/user/create.rs new file mode 100644 index 0000000..ac8d762 --- /dev/null +++ b/src/api/user/create.rs @@ -0,0 +1,60 @@ +use rocket::{ + // http::Status, + serde::{json::Json, Deserialize}, + State, +}; +use crate::api::sql::Database; + +use rand::{distributions::Alphanumeric, Rng}; + +/// Struct representing the user. +#[derive(Deserialize)] +#[serde(crate = "rocket::serde")] +pub struct User<'r> { + /// The username of the user. + username: &'r str, + /// The password of the user. + password: &'r str, +} + +/// Creates a new user in the database. +/// +/// # Endpoint +/// +/// POST `/api/user/create` +/// +/// # Parameters +/// +/// - `user`: Data about the user to be created. +/// - `database`: Instance of the database state. +/// +/// # Returns +/// +/// Upon successful creation, returns a status indicating success (`200 OK`). +/// If a database error occurs, returns a `500 Internal Server Error`. +/// +/// # Example Request +/// +/// ```bash +/// curl -X POST -H "Content-Type: application/json" -d '{"username":"example_user","password":"example_password"}' https://url/api/user/create +/// ``` +#[post("/create", data = "")] +pub async fn create(user: Json>, database: &State) { + 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); + } + } +} \ No newline at end of file diff --git a/src/api/user/login.rs b/src/api/user/login.rs new file mode 100644 index 0000000..7e9c729 --- /dev/null +++ b/src/api/user/login.rs @@ -0,0 +1,69 @@ +use rocket::{ + http::Status, + serde::{json::Json, Serialize}, + State, +}; +use crate::api::sql::Database; + +/// Logs in the user and returns the user ID and secret. +/// +/// # Endpoint +/// +/// GET `/api/user/login//` +/// +/// # Path Parameters +/// +/// - `username`: The username of the user. +/// - `password`: The password of the user. +/// +/// # Returns +/// +/// Returns the user ID and secret upon successful login. +/// +/// If the login credentials are invalid, returns a `401 Unauthorized` status. +/// +/// If any database-related error occurs, returns a `500 Internal Server Error`. +/// +/// # Example Request +/// +/// ```bash +/// curl -X GET "https://url/api/login/example_user/example_password" +/// ``` +/// +/// # Example Response +/// +/// ```json +/// { +/// "user_id": "1234567890", +/// "secret": "abcdefghijklmnopqrstuvwxyz" +/// } +/// ``` +#[get("/login//")] +pub async fn login(username: &str, password: &str, database: &State) -> Result, Status> { + match database + .find_user(username, &sha256::digest(password)) + .await + { + Err(why) => { + println!("A database error occured during login for {username}, {why}"); + return Err(Status::InternalServerError); + } + Ok(user) => match user { + None => return Err(Status::Unauthorized), + Some(user) => Ok(Json(LoginResponse { + user_id: user.0, + secret: user.1, + })), + }, + } +} + +/// Struct representing the response of login API. +#[derive(Serialize)] +#[serde(crate = "rocket::serde")] +pub struct LoginResponse { + /// The user ID of the logged-in user. + user_id: u32, + /// The secret key associated with the user. + secret: String, +} \ No newline at end of file diff --git a/src/api/user/mod.rs b/src/api/user/mod.rs new file mode 100644 index 0000000..2b6d1aa --- /dev/null +++ b/src/api/user/mod.rs @@ -0,0 +1,4 @@ +pub mod login; +pub mod tests; +pub mod test; +pub mod create; \ No newline at end of file diff --git a/src/api/user/test.rs b/src/api/user/test.rs new file mode 100644 index 0000000..ac76a9f --- /dev/null +++ b/src/api/user/test.rs @@ -0,0 +1,146 @@ +use rocket::serde::{json::Json, Deserialize}; +use rocket::State; +use rocket::http::Status; +use crate::api::sql::Database; +use crate::api::sql::Test; + +/// the datascructure that the webserver will recieve +/// when a post is made to the http://url/api/post_test route +#[derive(Deserialize)] +#[serde(crate = "rocket::serde")] +pub struct PostTest<'r> { + pub test_type: &'r str, + pub test_length: u32, + pub test_time: u32, + pub test_seed: i64, + pub quote_id: i32, + pub wpm: u8, + pub accuracy: u8, + pub user_id: u32, + pub secret: &'r str, +} + +/// Takes a test a user has completed and adds it tho the database +/// +/// # Endpoint +/// +/// GET `/api/user/test` +/// +/// # Path Parameters +/// +/// `test_type`: Type of typing test the user has completed (CURRENTLY ONLY "WORDS") +/// `test_length`: Number of characters the user typed +/// `test_time`: Time in seconds that the test took to complet +/// `test_seed`: Seed used to generate the test (NOT CURRENTLY IN USE) +/// `quote_id`: Quote ID that the user typed (NOT CURRENTLY IN USE) +/// `wpm`: Number of words per minute the user typed (A WORD IS COUNTED AS 5 CHARACTERS) +/// `accuracy`: Percentage accuracy that the user achieved in the test +/// `user_id`: User ID of the user whose completed the test +/// `secret`: Secret key for authentication. +/// +/// # Returns +/// +/// Returns the latest test of the user if the user is authenticated and the test is found. +/// +/// `401 UNAUTHORIZED` - The user has failed to be authenticated +/// `500 INTERNAL SERVER ERROR` - The test could not be added to the database +/// +/// # Example Request +/// +/// ```bash +/// curl -X GET "https://url/api/user/latest_user_test/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 +/// } +/// ``` +#[post("/test", data = "")] +pub async fn post_test(test: Json>, database: &State) -> Status { + match database.authenticate_user(test.user_id, test.secret).await { + Err(_) => return Status::InternalServerError, + Ok(authenticated) => { + if !authenticated { return Status::Unauthorized } + } + } + + 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}"); + } + } + + Status::Accepted +} + +/// Retrieves the latest test associated with a specific user from the database and returns it as JSON. +/// +/// # Endpoint +/// +/// GET `/api/user/latest_test//` +/// +/// # Path Parameters +/// +/// - `user_id`: User ID of the user whose latest test needs to be retrieved. +/// - `secret`: Secret key for authentication. +/// +/// # Returns +/// +/// Returns the latest test of the user if the user is authenticated and the test is found. +/// +/// If the user authentication fails, returns a `401 Unauthorized` status. +/// +/// If the test is not found or any database-related error occurs, returns a `404 Not Found` status. +/// +/// # Example Request +/// +/// ```bash +/// curl -X GET "https://url/api/user/latest_user_test/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 +/// } +/// ``` +#[get("/test//")] +pub async fn get_latest_test(user_id: u32, secret: &str, database: &State) -> Result, 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_test(user_id).await { + Err(why) => { + println!(" >> A database error occured during getting_tests, {why}"); + Err(Status::NotFound) + } + Ok(test) => { + println!(" >> Succesfully Found latestTest for User {user_id}"); + Ok(Json(test)) + } + } +} \ No newline at end of file diff --git a/src/api/user/tests.rs b/src/api/user/tests.rs new file mode 100644 index 0000000..ff8a65b --- /dev/null +++ b/src/api/user/tests.rs @@ -0,0 +1,74 @@ +use rocket::{ + http::Status, + serde::json::Json, + State, +}; +use crate::api::sql::{Database, Test}; + +/// Retrieves tests associated with a specific user from the database and returns them as a JSON array. +/// +/// # Endpoint +/// +/// GET /api/get_tests// +/// +/// # 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://url/api/user/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("/tests//")] +pub async fn tests(user_id: u32, secret: &str, database: &State) -> Result>, 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}", tests.len()); + Ok(Json(tests)) + } + } +} \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 6a6a89a..19606fd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -22,10 +22,20 @@ mod api; mod catchers; mod redirects; -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 crate::api::{ + sql::Database, + + user::{ + create::create, + login::login, + tests::tests, + test::get_latest_test, + test::post_test, + }, + test::generate_test, + leaderboard::leaderboard, + ping::ping, +}; use crate::catchers::not_found::api_not_found; use crate::catchers::not_found::frontend_not_found; @@ -33,16 +43,6 @@ use crate::catchers::not_found::documentation_not_found; use crate::redirects::leaderboard_redirect; use crate::redirects::typing_redirect; - -// Imports for sql, see sql.rs for more information - -/// Test api route that returns hello world. -/// Acessible from http://url/test -#[get("/")] -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] @@ -53,19 +53,22 @@ async fn rocket() -> Rocket { // 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![ - sign_up, + .mount("/api/user", routes![ + create, login, - - new_test, - create_test, - get_tests, - leaderboard + tests, + post_test, + get_latest_test + ]) + .mount("/api", routes![ + ping, + leaderboard, + generate_test ]) .register("/api", catchers![api_not_found]) + .mount("/api/documentation", FileServer::from(relative!("documentation/html"))) + .register("/api/documentation", catchers![documentation_not_found]) // hosts the fileserver .mount("/typing", FileServer::from(relative!("public"))) @@ -78,5 +81,5 @@ async fn rocket() -> Rocket { .mount("/leaderboard", routes![leaderboard_redirect]) // testing only, should return "Hello world" - .mount("/test", routes![test]) + }