diff --git a/.gitignore b/.gitignore index 5a12ba4..753be59 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ .vscode users.json message.zsh +TODO.md users_db/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 593c527..ac16f98 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,19 @@ +## 0.6.0 +- Remove deprecated API +- `/api/register` & `/api/login` now use JSON data +- Changing user info now uses Enum (Name, Pin, or Pronouns) + +### 0.5.2 +- When changing a username, it now deletes the previous user instead of creating a new one +- Database functions should now use some error handling +- Functions use `db_read_user()` instead of reading in the whole database + +### 0.5.1 +- `/api/logout` API to delete session token +- Add basic support for different message types +- Messages now use unix timestamps + - Backend finds timestamp + ## 0.5.0 - Most actions should now fail on a NULL token - Cookie should now expire after a week diff --git a/Cargo.lock b/Cargo.lock index 0845d70..2b12326 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -228,9 +228,9 @@ dependencies = [ [[package]] name = "crypto-mac" -version = "0.10.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4857fd85a0c34b3c3297875b747c1e02e06b6a0ea32dd892d8192b9ce0813ea6" +checksum = "bff07008ec701e8028e2ceb8f83f0e4274ee62bd2dbdc4fefff2e9a91824081a" dependencies = [ "generic-array", "subtle", @@ -301,9 +301,9 @@ dependencies = [ [[package]] name = "fastrand" -version = "1.4.1" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77b705829d1e87f762c2df6da140b26af5839e1033aa84aa5f56bb688e4e1bdb" +checksum = "b394ed3d285a429378d3b384b9eb1285267e7df4b166df24b7a6939a04dc392e" dependencies = [ "instant", ] @@ -572,9 +572,9 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] name = "libc" -version = "0.2.97" +version = "0.2.98" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12b8adadd720df158f4d70dfe7ccc6adb0472d7c55ca83445f6a5ab3e36f8fb6" +checksum = "320cfe77175da3a483efed4bc0adc1968ca050b098ce4f2f1c13a56626128790" [[package]] name = "lock_api" @@ -807,7 +807,7 @@ checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" [[package]] name = "pogchat" -version = "0.5.0" +version = "0.6.0" dependencies = [ "bincode", "chrono", @@ -1062,7 +1062,7 @@ checksum = "963a7dbc9895aeac7ac90e74f34a5d5261828f79df35cbed41e10189d3804d43" dependencies = [ "proc-macro2 1.0.27", "quote 1.0.9", - "syn 1.0.73", + "syn 1.0.74", ] [[package]] @@ -1148,9 +1148,9 @@ dependencies = [ [[package]] name = "syn" -version = "1.0.73" +version = "1.0.74" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f71489ff30030d2ae598524f61326b902466f72a0fb1a8564c001cc63425bcc7" +checksum = "1873d832550d4588c3dbc20f01361ab00bfe741048f71e3fecf145a7cc18b29c" dependencies = [ "proc-macro2 1.0.27", "quote 1.0.9", @@ -1178,9 +1178,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.2.0" +version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b5220f05bb7de7f3f53c7c065e1199b3172696fe2db9f9c4d8ad9b4ee74c342" +checksum = "848a1e1181b9f6753b5e96a092749e29b11d19ede67dfbbd6c7dc7e0f49b5338" dependencies = [ "tinyvec_macros", ] @@ -1259,9 +1259,9 @@ checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" [[package]] name = "universal-hash" -version = "0.4.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8326b2c654932e3e4f9196e69d08fdf7cfd718e1dc6f66b347e6024a0c961402" +checksum = "9f214e8f697e925001e66ec2c6e37a4ef93f0f78c2eed7814394e10c62025b05" dependencies = [ "generic-array", "subtle", diff --git a/Cargo.toml b/Cargo.toml index 26d401a..f4a21b4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pogchat" -version = "0.5.0" +version = "0.6.0" authors = ["Erin Nova "] edition = "2018" diff --git a/README.md b/README.md index 70fa754..fcfc2a9 100644 --- a/README.md +++ b/README.md @@ -1,54 +1,95 @@ # Chat Registration System -The basic backend code needed to register & login to a chat system (to be built). -Send it the unhashed username and pin, and it'll store it in the database with the pin hashed with SHA1. +A simple chat system for built for maya's livestream. +Provides a simple API for user authentication, and chat functions. +Frontend & backend code stored here. -## API Documentation +## Auth API Documentation -`POST /api/register {"name":"","pin":"","pronouns":""}` Register a user if they don't already exist +Most API functions will return JSON in the following format: -`POST /api/register///` Register the username with the pin provided if it doesn't already exist -Returns status & reason json. +`status`: either `ok` if action succeeded, or `fail` otherwise. -`GET /api/users/` Check if the user exists -Returns either +`reason`: More info about why the action failed. -`{ - "status": "fail", - "reason": "user not found", -}` +### Register & Login: -or +`POST /api/register` with JSON body values of: `name`, `pin`, `pronouns`. -`{ - "status": "ok", - "user": { - "name": "", - "pronouns": "", +Will return JSON with `status` and `reason`. + +`POST /api/login` with JSON body values of: `name`, `pin`. + +Will return JSON with `status` and `reason`. + +Will set a private cookie named `token` which is used for authentication. + +### Change User Information + +User information such as name, pin, and pronouns, can be changed currently one at a time. + +`POST /api/change` with JSON body values of: `name`, `changed_event`, `new_event`. + +`name` the user's current username. used for authentication. + +`changed_event` which event to change. value can be one of: `Name`, `Pin`, `Pronouns`. + +`new_event` the new value for the changed event. + +User is authenticated via token. + +### Check if User is Still Logged in + +Instead of having to save the pin and re-login every time to check wether they're logged in, you can just check via the token. + +`GET /api/token/` where `` is the current username. + +Will return JSON with `status` and `reason`. + +### Logout + +This API will remove the cookie from the client, as well as invalidating the token serverside. + +`POST /api/logout` with JSON body values of: `name`. + +Will use the current token as authentication. + +Will return JSON with `status` and `reason`. + +### Get Info About A User + +This API will return info about a user on success. + +`GET /api/users/` + +On success returns JSON in format: + + `status`: `ok` + `user`: + `name`: user's name + `pronouns`: user's pronouns + `role`: the users role, one of either `Normal`, `Moderator`, or `Admin + +eg: + +``` +{ + status: "ok", + user: { + name: "example", + pronouns: "they/them", + role: "Normal", }, -}` +} +``` -`GET /api/token/` Check if the current token matches the user provided +## Chat API Documentation -`GET /api/users//` Check if the user exists, and if the pin provided matches -Returns status & reason json. +`POST /api/message/send {"name":"username","body":"message body"}` Post a message with JSON body values of: `name` & `body` -`POST /api/users/change {"name":"","pin":"","changed_event":"name/pin/pronouns","new_event":""` Change a users details via a json post. +Will return JSON with `status` and `reason`. -eg. `POST /api/users/change {"name":"example","pin":"10","changed_event":"name","new_event":"test"` to change the user "example"'s name to "test" - -DEPRECATED `POST /api/users/change////` Change a users pin/name -Returns status & reason json. - -`POST /api/logout {"name":""}` to logout a user if the token matches - - -## Chat Documentation - -`POST /api/message/send {"name":"username","body":"message body","date":"yyyy-mm-dd"}` Post a json message. -Returns status & reason json. - -`GET /api/message/messages.json` Get a json file of all the messages +`GET /api/message/messages.json` Returns a json file of all the messages ## Chat Planning @@ -65,10 +106,10 @@ Whenever user sends a message, client will send message & token and backend will - [x] Basic messaging system - [x] Finish up `chat::create_message()` - [x] Create `chat::fetch_messages()` - - [ ] Use unix timestamp for date + - [x] Use unix timestamp for date - [ ] Create `chat::delete_message()` - [x] Switch to using sled database to store users - - [ ] Error handling + - [x] Error handling - [x] Token generation & storage - [x] Sets cookie - [x] Store token in json @@ -79,11 +120,15 @@ Whenever user sends a message, client will send message & token and backend will - [x] Fail on NULL token - [x] Pronouns - [x] Set pronouns - - [ ] Change pronouns - - [ ] Multiple sets of pronouns + - [x] Change pronouns +- [x] make changed_event Enum, use token instead of pin - [ ] Some form of plural support? - [ ] User management (banning, etc.) + - [x] User roles (admin, mod, etc.) + - [ ] Commands to affect users - [ ] Blacklist words from chat/names - [ ] More advanced chat features - - [ ] Different types of message events? eg. default, announcement, command + - [x] Different types of message events? eg. default, announcement, command + - [ ] Types will display differently? eg. announcements pinned to top? + - [ ] Have different commands? - [ ] Emote support? diff --git a/message.zsh b/message.zsh index 267cabb..915a2a2 100755 --- a/message.zsh +++ b/message.zsh @@ -1 +1 @@ -http POST 'http://localhost:8000/api/message/send' name=erin body="nyaa uwu" date="2021-07-21" +http POST 'http://localhost:8000/api/mod' name=erin action=Ban target=sarah diff --git a/src/auth.rs b/src/auth.rs index fe4e5bc..226a5bb 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -1,66 +1,33 @@ extern crate log; -use crate::file_io::{db_add, db_write, db_read}; -use rocket::http::{Cookie, Cookies, SameSite}; -use crate::user::User; +use crate::file_io::*; +use rocket::http::{Cookie, Cookies}; +use crate::user::*; use rocket_contrib::json::{Json, JsonValue}; use random_string::generate; extern crate sha1; use serde::Deserialize; -#[get("/")] -pub fn index() -> &'static str { - "API Info: - - `POST /api/register///` Register the username with the pin provided if it doesn't already exist - - `GET /api/users/` Check if the user exists - - `GET /api/users//` Check if the user exists, and if the pin provided matches - - `POST /api/users/change////` Change a users name and/or pin - - `GET /api/about/name/` Get the name of a user - - `GET /api/about/pronouns/` Get the pronouns of a user" -} - -#[derive(Deserialize, Debug)] -pub struct RegisterEvent { - pub name: String, - pub pin: String, - pub pronouns: String, -} - // Post request to register a user and pin -#[post("/register", format = "json", data = "")] -pub fn register_user(info: Json) -> JsonValue { - let users: Vec = db_read(); // Create an array of users out of parsed json - for i in &users { - // loop through elements of the vector - if i.name == info.name.to_lowercase() { - warn!("Cannot create user {}! User is already in system.", i.name); - return json!({ - "status": "fail", - "reason": "user already exists", - }); +#[post("/register", format = "json", data = "")] +pub fn register(data: Json) -> JsonValue { + // check if the user exists + if let Some(user) = db_read_user(&data.name).ok().flatten() { + warn!("Cannot create user {}! User is already in system.", data.name); + return json!({ + "status": "fail", + "reason": "user already exists", + }); + } else { + let pin_hashed = sha1::Sha1::from(&data.pin).digest().to_string(); // hash the pin + + let new_user: User = User { + name: data.name.to_string().to_lowercase(), + pin_hashed, + pronouns: data.pronouns.to_string().to_lowercase(), + session_token: "NULL".to_string(), + role: UserType::Normal, + }; - } - - let pin_hashed = sha1::Sha1::from(&info.pin.to_string()).digest().to_string(); // hash the pin - - let new_user: User = User { - name: info.name.to_string().to_lowercase(), - pin_hashed: pin_hashed, - pronouns: info.pronouns.to_string().to_lowercase(), - session_token: "NULL".to_string(), - }; // append the user to the vec - - /* - // append to the json file - match append_json(&new_user) { - Err(why) => panic!("couldn't append json: {}", why), - Ok(()) => info!("Succesfully appended to json"), - };*/ db_add(&new_user); info!( @@ -72,25 +39,20 @@ pub fn register_user(info: Json) -> JsonValue { "status": "ok", "reason": format!("user {} registered", new_user.name.to_string().to_lowercase()), }); + } } -fn create_token(name: String, mut users: Vec) -> String { +fn create_token(name: String, mut user: User) -> String { let charset = "1234567890abcdefghijklmnopqrstuvwxyz"; - for i in 0..users.len() { - if users[i].name == name { - users[i].session_token = generate(12, charset); - /* - match write_json(&users) { - Err(why) => panic!("coudln't write to file: {}", why), - Ok(()) => info!("succesfully wrote to file"), - };*/ - db_write(&users); - info!("succesfully created token for user {}", name); - let token = users[i].session_token.clone(); - return token; - }; + if user.name == name { + user.session_token = generate(12, charset); + db_add(&user); + info!("succesfully created token for user {}", name); + let token = user.session_token.clone(); + return token; }; + warn!("something bad happened while creating a token and idk what"); return "NULL".to_string(); } @@ -98,159 +60,141 @@ fn create_token(name: String, mut users: Vec) -> String { // Check if user is properly logged in #[get("/token/")] pub fn check_token(name: String, mut cookies: Cookies) -> JsonValue { - let users: Vec = db_read(); - for i in &users { - if i.name == name.to_lowercase() { - let token = match cookies.get_private("token") { - None => { - warn!("couldn't get token cookie!"); - return json!({ - "status": "fail", - "reason": "could not read cookie", - }); - }, - Some(token) => token, - }; - if token.value() == "NULL" { - warn!("NULL token!"); + // check if the user is in the system + if let Some(user) = db_read_user(&name).ok().flatten() { + // get the token from the cookie + let token = match cookies.get_private("token") { + None => { + warn!("couldn't get token cookie!"); return json!({ "status": "fail", - "reason": "NULL token", + "reason": "could not read cookie", }); - } else if token.value() == i.session_token { - info!("user {} has correct session token", name); - return json!({ - "status": "ok", - "reason": "correct token", - }); - } else { - info!("user {} has incorrect token!", name); - return json!({ - "status": "fail", - "reason": "incorrect token", - }); - } + }, + Some(token) => token, + }; + + // check the token value + if token.value() == "NULL" { + warn!("NULL token!"); + return json!({ + "status": "fail", + "reason": "NULL token", + }); + } else if token.value() == user.session_token { + info!("user {} has correct session token", name); + return json!({ + "status": "ok", + "reason": "correct token", + }); + } else { + info!("user {} has incorrect token!", name); + return json!({ + "status": "fail", + "reason": "incorrect token", + }); } - } + } else { warn!("user {} not found", name); return json!({ "status": "fail", "reason": "user not found", }); -} - -// logout event struct -#[derive(Deserialize, Debug)] -pub struct LogoutEvent { - pub name: String, + } } // Logout API #[post("/logout", format = "json", data = "")] pub fn logout(info: Json, mut cookies: Cookies) -> JsonValue { - let mut users: Vec = db_read(); - for i in 0..users.len() { - if info.name.to_lowercase() == users[i].name { - let token = match cookies.get_private("token") { - None => { - warn!("couldn't get token cookie!"); - return json!({ - "status": "fail", - "reason": "could not read cookie", - }); - }, - Some(token) => token, - }; - if token.value() == "NULL" { - warn!("NULL token!"); + if let Some(mut user) = db_read_user(&info.name.to_lowercase()).ok().flatten() { + let token = match cookies.get_private("token") { + None => { + warn!("couldn't get token cookie!"); return json!({ "status": "fail", - "reason": "NULL token", + "reason": "could not read cookie", }); - } else if token.value() == users[i].session_token { - - cookies.remove_private(Cookie::named("token")); - users[i].session_token = "NULL".to_string(); - info!("logged out user {}", info.name); - return json!({ - "status": "ok", - "reason": "logged out", - }); - } else { - warn!("token does not match! cannot logout"); - return json!({ - "status": "fail", - "reason": "token does not match", - }); - } + }, + Some(token) => token, + }; + if token.value() == "NULL" { + warn!("NULL token!"); + return json!({ + "status": "fail", + "reason": "NULL token", + }); + } else if token.value() == user.session_token { + cookies.remove_private(Cookie::named("token")); + user.session_token = "NULL".to_string(); + db_add(&user); + info!("logged out user {}", info.name); + return json!({ + "status": "ok", + "reason": "logged out", + }); + } else { + warn!("token does not match! cannot logout"); + return json!({ + "status": "fail", + "reason": "token does not match", + }); } + } else { + warn!("failed to log out user {}, user not found", info.name); + return json!({ + "status": "fail", + "reason": "user not found", + }); } - warn!("logged out user {}, user not found", info.name); - return json!({ - "status": "fail", - "reason": "user not found", - }); } // Check if pin matches user -#[get("/users//")] -pub fn login(mut cookies: Cookies, name: String, pin: i32) -> JsonValue { - let users: Vec = db_read(); - let hashed_pin_input = sha1::Sha1::from(&pin.to_string()).digest().to_string(); - for i in &users { - // loop through the vector - if i.name == name.to_lowercase() { - if i.pin_hashed == hashed_pin_input { - info!("pin correct for user {}", i.name); +#[post("/login", format = "json", data = "")] +pub fn login(data: Json, mut cookies: Cookies) -> JsonValue { + if let Some(user) = db_read_user(&data.name.to_lowercase()).ok().flatten() { + let hashed_pin_input = sha1::Sha1::from(&data.pin.to_string()).digest().to_string(); - // Create token for user & set a cookie - let token = create_token(i.name.clone(), users); - let cookie = Cookie::build("token", token) - .path("/") - .same_site(SameSite::Strict) - .secure(true) - .finish(); - cookies.remove_private(Cookie::named("token")); - cookies.add_private(cookie); - info!("set the token cookie"); + if user.pin_hashed == hashed_pin_input { // check if pin hash matches + info!("pin correct for user {}", &user.name); - return json!({ - "status": "ok", - "reason": "pin matches", - }); - } else { - cookies.remove_private(Cookie::named("token")); - info!("removed private cookie"); - warn!("pin incorrect for user {}", i.name); - return json!({ - "status": "fail", - "reason": "incorrect pin", - }); - }; + // Create token for user & set a cookie + let token = create_token(user.name.clone(), user); + let cookie = Cookie::build("token", token) + .path("/") + .finish(); + cookies.remove_private(Cookie::named("token")); + cookies.add_private(cookie); + info!("set the token cookie"); + + return json!({ + "status": "ok", + "reason": "pin matches", + }); + } else { + cookies.remove_private(Cookie::named("token")); + info!("removed private cookie"); + warn!("pin incorrect for user {}", user.name); + return json!({ + "status": "fail", + "reason": "incorrect pin", + }); }; + } else { + cookies.remove_private(Cookie::named("token")); + info!("removed private cookie"); + warn!( + "cannot check pin for user {} as they do not exist", + data.name.to_string().to_lowercase() + ); + return json!({ + "status": "fail", + "reason": format!("user {} doesn't exist", data.name.to_string().to_lowercase()), + }); } - cookies.remove_private(Cookie::named("token")); - info!("removed private cookie"); - warn!( - "cannot check pin for user {} as they do not exist", - name.to_string().to_lowercase() - ); - return json!({ - "status": "fail", - "reason": format!("user {} doesn't exist", name.to_string().to_lowercase()), - }); -} - -#[derive(Deserialize, Debug)] -pub struct ChangeEvent { - pub name: String, - pub pin: String, - pub changed_event: String, - pub new_event: String, } // Change info about a user -#[post("/users/change", format = "json", data = "")] +#[post("/change", format = "json", data = "")] pub fn change_info(input: Json, mut cookies: Cookies) -> JsonValue { // read in the users & hash the pin let mut users: Vec = db_read(); @@ -274,134 +218,61 @@ pub fn change_info(input: Json, mut cookies: Cookies) -> JsonValue }); } - // loop through the users - for i in 0..users.len() { - if input.name.to_lowercase() == users[i].name { // if user found... - if token.value() == users[i].session_token { // & if token matches: - if input.changed_event == "name" { + // find the user + if let Some(mut user) = db_read_user(&input.name).ok().flatten() { + if token.value() == user.session_token { // & if token matches: + match input.changed_event { + ChangeEventType::Name => { + // remove the user first + db_remove(&user); // change the name - users[i].name = input.new_event.clone(); + user.name = input.new_event.clone(); info!("changed name of {} to {}", input.name, input.new_event); - db_write(&users); + db_add(&user); return json!({ "status": "ok", "reason": format!("changed name of {} to {}", input.name, input.new_event), }); - } else if input.changed_event == "pin" { + }, + ChangeEventType::Pin => { // change the pin let new_hashed_pin = sha1::Sha1::from(&input.new_event).digest().to_string(); - users[i].pin_hashed = new_hashed_pin.clone(); - db_write(&users); + user.pin_hashed = new_hashed_pin.clone(); + db_add(&user); info!("changed pin of {}", input.name); return json!({ "status": "ok", "reason": "changed pin", }); - } else if input.changed_event == "pronouns" { + }, + ChangeEventType::Pronouns => { // change the pronouns - users[i].pronouns = input.new_event.clone(); + user.pronouns = input.new_event.clone(); info!("changed pronouns of {} to {}", input.name, input.new_event); - db_write(&users); + db_add(&user); return json!({ "status": "ok", "reason": "successfully changed pronouns", }); - }; - } else { - warn!("incorrect pin for user {}", input.name); - return json!({ - "status": "fail", - "reason": "incorrect pin", - }); + }, }; - }; - }; - warn!("couldn't change users info, user does not exist"); - return json!({ - "status": "fail", - "reason": "user doesn't exist", - }); -} - -// Change a users pin/name -#[post("/users/change////")] -pub fn change(name: String, pin: i32, new_name: String, new_pin: i32) -> JsonValue { - let mut users: Vec = db_read(); - - let hashed_pin_input = sha1::Sha1::from(&pin.to_string()).digest().to_string(); - - // Loop over elements in vector - for i in 0..users.len() { - if users[i].name == name.to_lowercase() { - // make sure name exists - if hashed_pin_input == users[i].pin_hashed { - // check if token is correct - // Check wether to change name or name+pin - if users[i].name == new_name.to_lowercase() { - // check if new name already exists - users[i].pin_hashed = sha1::Sha1::from(&new_pin.to_string()).digest().to_string(); - /* - match write_json(&users) { - Err(why) => panic!("Cannot write to json! {}", why), - Ok(()) => info!("succesfully wrote to json file"), - }*/ - db_write(&users); - info!("Changed pin of {}", name.to_string().to_lowercase()); - return json!({ - "status": "ok", - "reason": format!("changed {}'s pin", name.to_string().to_lowercase()), - }); - } else { - // check if new name already exists - for n in &users { - if n.name == new_name.to_lowercase() { - warn!( - "Could not change name of {} to {}, as new name is already taken.", - name.to_lowercase(), - new_name.to_lowercase() - ); - return json!({ - "status": "fail", - "reason": format!("new name {} is already taken", new_name.to_lowercase()), - }); - } - } - users[i].name = new_name.to_string().to_lowercase(); - users[i].pin_hashed = - sha1::Sha1::from(&new_pin.to_string()).digest().to_string(); - /* - match write_json(&users) { - Err(why) => panic!("couldn't write to json file! {}", why), - Ok(()) => info!("succesfully wrote to json file"), - }*/ - db_write(&users); - info!( - "Changed name of {} to {}. New pin hash is {}", - name.to_string(), - users[i].name.to_string(), - users[i].pin_hashed.to_string() - ); - return json!({ - "status": "ok", - "reason": "successfully changed name and/or pin", - }); - } - } else { - warn!("Incorrect token for user {}!", name.to_string()); - return json!({ - "status": "fail", - "reason": "incorrect token for user", - }); - } - } + } else { + warn!("incorrect pin for user {}", input.name); + return json!({ + "status": "fail", + "reason": "incorrect pin", + }); + }; + } else { + warn!("couldn't change users info, user does not exist"); + return json!({ + "status": "fail", + "reason": "user doesn't exist", + }); } - warn!( - "User {} not found, could not change pin and/or name.", - name.to_string() - ); return json!({ "status": "fail", - "reason": format!("user {} not found", name.to_string().to_lowercase()), + "reason": "idk", }); } @@ -419,6 +290,7 @@ pub fn get_user(name: String) -> JsonValue { "user": { "name": user.name, "pronouns": user.pronouns, + "role": user.role, }, }), None => json!({ @@ -427,3 +299,59 @@ pub fn get_user(name: String) -> JsonValue { }), } } + +/* User Management */ +#[post("/mod", format = "json", data = "")] +pub fn moderation_actions(data: Json, mut cookies: Cookies) -> JsonValue { + let token = match cookies.get_private("token") { + None => { + warn!("couldn't get token cookie!"); + return json!({ + "status": "fail", + "reason": "could not read cookie", + }); + }, + Some(token) => token, + }; + if let Some(user) = db_read_user(&data.name.to_lowercase()).ok().flatten() { + if token.value() == "NULL" { // fail if token is NULL + warn!("NULL token!"); + return json!({ + "status": "fail", + "reason": "NULL token", + }); + } else if user.session_token == token.value() { // if token matches + if user.role == UserType::Normal { + match data.action { + ModActions::Kick => { + info!("kicked user {}", data.target) + }, + ModActions::Ban => info!("banned user {}", data.target), + _ => info!("F"), + }; + return json!({ + "status": "ok", + "reason": "completed action", + }); + } else { + warn!("user does not have sufficient permissions to perform that action!"); + return json!({ + "status": "fail", + "reason": "insufficient permissions", + }); + }; + } else { + warn!("token does not match!"); + return json!({ + "status": "fail", + "reason": "token does not match", + }) + }; + } else { + warn!("user not found"); + return json!({ + "status": "fail", + "reason": "user not found" + }); + } +} diff --git a/src/chat.rs b/src/chat.rs index 7d1d3f4..7912a65 100644 --- a/src/chat.rs +++ b/src/chat.rs @@ -4,7 +4,7 @@ use once_cell::sync::Lazy; use std::sync::Mutex; use crate::file_io::db_read; use rocket::http::{Cookie, Cookies}; -use crate::message::{Message, MessageInput}; +use crate::message::{Message, MessageInput, MessageType}; use rocket_contrib::json::{Json, JsonValue}; use chrono::prelude::*; use uuid::Uuid; @@ -23,35 +23,24 @@ pub fn fetch_messages() -> Json> { // Create full message object and write to file fn create_message(message: Json, file: &str, user: &User) -> JsonValue { + let event_type = match message.body.chars().nth(0).unwrap() { + '/' => MessageType::Command, + ':' => MessageType::Emote, + _ => MessageType::Normal, + }; + // create full message object - // append message to file - - // Create proper datetime format out of string - let date_split: Vec<&str> = message.date.split("-").collect(); - - let year: i32 = match date_split[0].trim().parse() { // extract year - Err(why) => panic!("could not extract year from given date: {}", why), - Ok(year) => year, - }; - - let month: u32 = match date_split[1].trim().parse() { // extract month - Err(why) => panic!("could not extract month from given date: {}", why), - Ok(month) => month, - }; - - let day: u32 = match date_split[2].trim().parse() { // extract day - Err(why) => panic!("could not extract day from given date: {}", why), - Ok(month) => month, - }; - - let date: DateTime = Utc.ymd(year, month, day).and_hms(9, 10, 11); let message_obj: Message = Message { id: Uuid::new_v4(), + event_type, user: user.name.to_owned(), body: message.body.to_string(), - created_at: date, + created_at: Utc::now(), }; info!("created mesage: {:?}", message_obj); + info!("Date is: {}", message_obj.created_at.to_rfc2822()); + + // append message to file let mut messages = MESSAGES.lock().unwrap(); messages.push(message_obj.to_owned()); return json!({ @@ -107,3 +96,20 @@ pub fn send_message(message: Json>, mut cookies: Cookies) -> Js }; check_token(token, message) } + +// Delete a message +/* +#[post("/message/delete", format = "json", data = "")] +pub fn delete_message(message: Json>, mut cookies: Cookies) -> JsonValue { + let token = match cookies.get_private("token") { + None => { + warn!("couldn't get token cookie!"); + return json!({ + "status": "fail", + "reason": "could not read cookie", + }); + }, + Some(token) => token, + }; +} +*/ diff --git a/src/file_io.rs b/src/file_io.rs index d173717..8a740c2 100644 --- a/src/file_io.rs +++ b/src/file_io.rs @@ -1,123 +1,7 @@ -use std::fs::{File, OpenOptions}; -use std::io::prelude::*; -use std::io::{self, BufRead}; -use std::path::Path; extern crate log; use crate::user::User; -use serde_json::Result; -fn read_lines

(filename: P) -> io::Result>> -where - P: AsRef, -{ - let file = File::open(filename)?; - Ok(io::BufReader::new(file).lines()) -} - -// Function to read json from file into the vector -pub fn read_json() -> Vec { - // Create path to file - let path = Path::new("users.json"); - let display = path.display(); - - let mut users: Vec = Vec::new(); // Create an empty vector of users - - // Read through the lines and append them to the array - if let Ok(lines) = read_lines(&path) { - for line in lines { - if let Ok(user) = line { - info!("read {} from json file {}", display, &user); - // Parse line from file into a data structure - let user: User = serde_json::from_str(&user).unwrap(); - users.push(user); - } - } - } - return users; -} - -// Function to append the last value of the users vector to the file -pub fn append_json(user: &User) -> Result<()> { - // Create a file to write to - let path = Path::new("users.json"); - let display = path.display(); - - let mut file = match OpenOptions::new() - .write(true) - .create(true) - .append(true) - .open(&path) - { - Err(why) => panic!("couldn't create {}: {}", display, why), - Ok(file) => file, - }; - - // Serialize the last user value - let user_json = serde_json::to_string(&user)?; - - // Write to the file - match file.write_all(user_json.as_bytes()) { - Err(why) => panic!("couldn't write to {}: {}", display, why), - Ok(_) => info!("succesfully wrote to {}", display), - }; - // Add newline - match file.write_all("\n".as_bytes()) { - Err(why) => panic!("couldn't write to {}: {}", display, why), - Ok(_) => info!("succesfully wrote newline to {}", display), - }; - Ok(()) -} - -// Function to write whole vector of users to file -pub fn write_json(users_list: &Vec) -> Result<()> { - // Create a file to write to - let path = Path::new("users.json"); - let display = path.display(); - - let mut file = match OpenOptions::new().write(true).create(true).open(&path) { - Err(why) => panic!("couldn't create {}: {}", display, why), - Ok(file) => file, - }; - - let mut users_json = String::new(); - for i in 0..users_list.len() { - // Serialize the users - users_json += &serde_json::to_string(&users_list[i])?; - if i != users_list.len()-1 { - // don't append newline if it's the last element - users_json += "\n"; - } - } - - // Write to the file - match file.write_all(users_json.as_bytes()) { - Err(why) => panic!("couldn't write to {}: {}", display, why), - Ok(_) => info!("succesfully wrote to {}", display), - }; - Ok(()) -} - -// test sled funtion -pub fn test_sled() { - // create test user - let user = User { - name: "erin".to_string(), - pin_hashed: "nyaa".to_string(), - pronouns: "she/her".to_string(), - session_token: "NULL".to_string(), - }; - // open database - let db: sled::Db = sled::open("my_db").unwrap(); - let bytes = bincode::serialize(&user).unwrap(); - db.insert(&user.name, bytes).unwrap(); - match db.get(user.name).unwrap() { - Some(bytes) => { - let read_user: User = bincode::deserialize(&bytes).unwrap(); - println!("username: {}, pronouns: {}", read_user.name, read_user.pronouns); - }, - None => (), - } -} +type MyErrorType = Box; // add a user to the database pub fn db_add(user: &User) { @@ -138,6 +22,12 @@ pub fn db_write(users_list: &Vec) { info!("wrote all users to db"); } +// remove a user from the database +pub fn db_remove(user: &User) { + let db: sled::Db = sled::open("users_db").unwrap(); + db.remove(&user.name); +} + // read all users from the database pub fn db_read() -> Vec { let db: sled::Db = sled::open("users_db").unwrap(); @@ -149,3 +39,17 @@ pub fn db_read() -> Vec { } return users; } + +// read one user from the database +pub fn db_read_user(user: &str) -> std::result::Result, MyErrorType> { + let db: sled::Db = sled::open("users_db")?; + let entry = db.get(user)?; + if let Some(user_entry) = entry { + let read_user: User = bincode::deserialize(&user_entry)?; + info!("read user {} from db", read_user.name); + Ok(Some(read_user)) + } else { + warn!("user {} not found in db!", user); + Ok(None) + } +} diff --git a/src/main.rs b/src/main.rs index 1efa8d4..fbba3eb 100644 --- a/src/main.rs +++ b/src/main.rs @@ -28,16 +28,15 @@ fn main() { .mount( "/api", routes![ - auth::index, auth::get_user, - auth::register_user, + auth::register, auth::login, - auth::change, chat::send_message, chat::fetch_messages, auth::change_info, auth::check_token, - auth::logout + auth::logout, + auth::moderation_actions ], ) .mount("/", StaticFiles::from("frontend")) diff --git a/src/message.rs b/src/message.rs index dd2d70d..58f1af3 100644 --- a/src/message.rs +++ b/src/message.rs @@ -7,12 +7,21 @@ use uuid::Uuid; pub struct MessageInput<'r> { pub name: &'r str, pub body: &'r str, - pub date: &'r str, + pub date: i64, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub enum MessageType { + Normal, + Announcement, + Emote, + Command, } #[derive(Debug, Deserialize, Serialize, Clone)] pub struct Message { pub id: Uuid, + pub event_type: MessageType, pub user: String, pub body: String, pub created_at: DateTime, diff --git a/src/user.rs b/src/user.rs index 915bd4a..c7fe77c 100644 --- a/src/user.rs +++ b/src/user.rs @@ -1,11 +1,76 @@ use serde::{Deserialize, Serialize}; +/* User Data */ +// enum of different user types +#[derive(Clone, Serialize, Deserialize, Debug, PartialEq)] +pub enum UserType { + Normal, + Moderator, + Admin, +} + // Struct to store basic user data #[derive(Clone, Serialize, Deserialize, Debug)] pub struct User { - pub name: String, - pub pin_hashed: String, - pub pronouns: String, - #[serde(rename = "sessionToken")] - pub session_token: String, + pub name: String, // unique username + pub pin_hashed: String, // sha1 hash of the pin + pub pronouns: String, // user's pronouns + pub session_token: String, // generated session token + pub role: UserType, // type/role of user +} + +/* Moderation Data */ +// enum of different moderator actions +#[derive(Deserialize, Debug)] +pub enum ModActions { + Kick, // Log the user out of their current session + Ban, // Remove the user + Demote, // Demote a user to a lower role + Premote, // Premote a user to a higher role +} + +// struct to use for json input +#[derive(Deserialize, Debug)] +pub struct ModerationAction { + pub name: String, // name of the moderator + pub action: ModActions, // what action to take + pub target: String, // who to take the action on +} + +/* Miscellaneous Events */ +// logout event struct +#[derive(Deserialize, Debug)] +pub struct LogoutEvent { + pub name: String, +} + +// register event struct +#[derive(Deserialize, Debug)] +pub struct RegisterEvent { + pub name: String, + pub pin: String, + pub pronouns: String, +} + +// login event struct +#[derive(Deserialize, Debug)] +pub struct LoginEvent { + pub name: String, + pub pin: String, +} + +// change event type +#[derive(Deserialize, Debug)] +pub enum ChangeEventType { + Name, + Pin, + Pronouns, +} + +// change info event struct +#[derive(Deserialize, Debug)] +pub struct ChangeEvent { + pub name: String, // name of the user + pub changed_event: ChangeEventType, // which event to change + pub new_event: String, // the new value for the event }