diff --git a/Cargo.lock b/Cargo.lock index 9bf04e0..78378b1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10,6 +10,7 @@ dependencies = [ "rand", "ring", "rusqlite", + "serde", "sha2", "tokio", "warp", @@ -142,6 +143,7 @@ dependencies = [ "iana-time-zone", "js-sys", "num-traits", + "serde", "wasm-bindgen", "windows-targets", ] diff --git a/Cargo.toml b/Cargo.toml index 8ca8e36..1f86773 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,8 +6,9 @@ edition = "2021" [dependencies] tokio = { version = "1", features = ["full"] } warp = "0.3" -chrono = "0.4" +chrono = { version = "0.4", features = ["serde"] } rusqlite = { version = "0.32", features = ["chrono", "bundled"] } sha2 = "0.10.8" ring = "0.17.8" rand = "0.8.5" +serde = { version = "1.0", features = ["derive"] } diff --git a/README.md b/README.md index 38a1a13..fb790be 100644 --- a/README.md +++ b/README.md @@ -4,18 +4,93 @@ AbleOS and related projects. The rest of this document is documentation for the API -## GET /abuelo/user/:username +## GET /user/:username Return information about a particular user in the following format: ```json { "success" : Boolean, "message" : String, - "username" : String, - "email" : String + "creation" : String, + "is_premium" : Boolean, } ``` - **success**: if the user is found successfully then the value returned is true - **message**: if success is false, contains an error message to give to the user -- **username**: +- **creation**: if success is true, contains the creation date of the account in the format +YYYY-MM-DD HH:MM +- **is_premium**: if succuss is true, contains whether or not the account is premium + +## POST /user/:username/update +Updates the records in the database +Request Format: +```json +{ + "username" : String?, + "password" : String, + "new_password" : String? +} +``` +(question marks indicate the value is nullable) +- **username**: The new username of the user +- **password**: The (plain-text currently but in future RSA encrypted) password of the user +- **new_password**: The new (plain-text currently but in future RSA encrypted) password of the user + +Response Format: +```json +{ + "success" : Boolean, + "message" : String, +} +``` +- **success**: if the user is updated successfully then the value returned is +true +- **message**: if success is false, contains an error message to give to the user + + +## POST /user/create +Adds a user to the database +Request Format: +```json +{ + "username" : String, + "password" : String, +} +``` +- **username**: The username of the newly created user +- **password**: The (plain-text currently but in future RSA encrypted) password of the newly created user + +Response Format: +```json +{ + "success" : Boolean, + "message" : String, +} +``` +- **success**: if the user is created successfully then the value returned is +true +- **message**: if success is false, contains an error message to give to the user + +## POST /user/auth +Authorizes the user +Request Format: +```json +{ + "username" : String, + "password" : String, +} +``` +- **username**: The username of the user +- **password**: The (plain-text currently but in future RSA encrypted) password of the user + +Response Format: +```json +{ + "success" : Boolean, + "message" : String, +} +``` +- **success**: if the user is authed successfully then the value returned is +true +- **message**: if success is false, contains an error message to give to the user diff --git a/src/account.rs b/src/account.rs index b6423e4..40b0d26 100644 --- a/src/account.rs +++ b/src/account.rs @@ -1,26 +1,36 @@ use chrono::{DateTime, Utc}; -pub type UserID = u128; +pub type UserID = u64; -pub enum AccountStatusState { - Offline, - Away, - DoNotDisturb, - Online, -} - -pub struct AccountStatus{ - state: AccountStatusState, - tagline: String, -} - - -pub struct Account{ +#[derive(serde::Serialize, serde::Deserialize, Debug)] +pub struct Account { username: String, user_id: UserID, - password_hash: String, - status: AccountStatus, - created_date: DateTime, + creation_time: DateTime, // Donator role premium: bool, } + +impl Account { + pub fn new( + username: String, + user_id: UserID, + creation_time: DateTime, + premium: bool, + ) -> Self { + Self { + username, + user_id, + creation_time, + premium, + } + } + + pub fn premium(&self) -> bool { + self.premium + } + + pub fn creation_time(&self) -> DateTime { + self.creation_time + } +} diff --git a/src/database.rs b/src/database.rs index 00baf83..e380b16 100644 --- a/src/database.rs +++ b/src/database.rs @@ -1,78 +1,134 @@ -use std::{rc::Rc, sync::Arc}; +use std::{fmt::Display, rc::Rc}; use chrono::{DateTime, Utc}; use rusqlite::{Connection, Result}; use sha2::{Digest, Sha256}; +use crate::account::{Account, UserID}; + pub struct Database { conn: Connection, } +#[derive(Debug)] +pub enum UserCreationError { + UsernameTaken, + DBError(rusqlite::Error), +} + +impl From for UserCreationError { + fn from(value: rusqlite::Error) -> Self { + Self::DBError(value) + } +} +impl std::error::Error for UserCreationError {} +impl Display for UserCreationError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + UserCreationError::UsernameTaken => { + write!(f, "Username was taken") + } + UserCreationError::DBError(e) => { + write!(f, "DBError: {}", e) + } + } + } +} + impl Database { pub fn new() -> Self { let conn = Connection::open("user_db.db3").unwrap(); // If this returns an error; it's prolly cuz the table already exists. - // That's fine and we can just let it error and the other queries will + // That's fine and we can just let it error and the other queries will // use the existing table instead // NOTE: Look at the other possible errors here - let _ = conn.execute( + let _val = conn.execute( "CREATE TABLE users ( user_id INTEGER PRIMARY KEY, username TINYTEXT NOT NULL, password_hash TINYTEXT NOT NULL, - creation_time DATETIME NOT NULL + creation_time DATETIME NOT NULL, is_premium BOOL NOT NULL, random_value INTEGER NOT NULL )", - ()); + (), + ); + Self { conn } } - pub fn add_user(&self, username : &str, password : &str) -> Result<()>{ - let password_hash = self.hash_password(username, password)?; + pub fn get_user(&self, username: &str) -> Result { + let (user_id, creation_time, premium): (UserID, DateTime, bool) = + self.conn.query_row( + "SELECT username, user_id, creation_time, is_premium FROM users WHERE username=?1", + [username], + |row| { + let user_id = row.get(1)?; + let creation_time = row.get(2)?; + let premium = row.get(3)?; + Ok((user_id, creation_time, premium)) + }, + )?; + Ok(Account::new( + username.to_string(), + user_id, + creation_time, + premium, + )) + } + + pub fn add_user(&self, username: &str, password: &str) -> Result<(), UserCreationError> { + if self.get_user(username).is_ok() { + return Err(UserCreationError::UsernameTaken); + } + let creation_time = Utc::now(); + let num = rand::random::(); + let password_hash = self.hash_password(password, creation_time, num); self.conn.execute( - "INSERT INTO person ( + "INSERT INTO users ( username, password_hash, creation_time, is_premium, random_value) VALUES (?1, ?2, ?3, ?4, ?5)", - (username, password_hash, Utc::now(), false, rand::random::()), + (username, password_hash, creation_time, false, num), )?; Ok(()) } - fn hash_password(&self, username : &str, password : &str) -> Result{ - let (creation_time, num) : (DateTime, u64) = self.conn.query_row( - "SELECT creation_time, random_value, name FROM person WHERE name=?1", - [username], - |row|{ - let creation_time = row.get(0)?; - let num = row.get(1)?; - Ok((creation_time, num)) - }, - )?; + fn hash_password(&self, password: &str, creation_time: DateTime, num: u64) -> String { let mut hasher = Sha256::new(); hasher.update(password); hasher.update(creation_time.format("%Y-%m-%d-%H-%M").to_string()); hasher.update(num.to_string()); - Ok(format!("{:x}", hasher.finalize())) + format!("{:x}", hasher.finalize()) } - pub fn check_login(&self, username : &str, password : &str) -> bool{ - let saved_password_hash : Result> = self.conn.query_row( - "SELECT password_hash, name FROM person WHERE name=?1", + pub fn check_login(&self, username: &str, password: &str) -> bool { + let result = self.conn.query_row( + "SELECT creation_time, random_value, username FROM users WHERE username=?1", + [username], + |row| { + let creation_time = row.get(0)?; + let num = row.get(1)?; + Ok((creation_time, num)) + }, + ); + if result.is_err() { + return false; + } + let (creation_time, num) = result.unwrap(); + let saved_password_hash: Result> = self.conn.query_row( + "SELECT password_hash, username FROM users WHERE username=?1", [username], |row| row.get::>(0), ); - if let Err(_) = saved_password_hash { + if saved_password_hash.is_err() { return false; } - *saved_password_hash.unwrap() == *password - + *saved_password_hash.unwrap() == self.hash_password(password, creation_time, num) } - } diff --git a/src/handlers.rs b/src/handlers.rs new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/handlers.rs @@ -0,0 +1 @@ + diff --git a/src/main.rs b/src/main.rs index 0df7802..576f3fd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,14 +1,11 @@ -use warp::Filter; +use routes::get_routes; mod account; mod database; +mod handlers; +mod routes; + #[tokio::main] async fn main() { - // GET /hello/warp => 200 OK with body "Hello, warp!" - let hello = warp::path!("hello" / String) - .map(|name| format!("Hello, {}!", name)); - - warp::serve(hello) - .run(([127, 0, 0, 1], 3030)) - .await; + warp::serve(get_routes()).run(([127, 0, 0, 1], 3030)).await; } diff --git a/src/routes.rs b/src/routes.rs new file mode 100644 index 0000000..1012f0f --- /dev/null +++ b/src/routes.rs @@ -0,0 +1,104 @@ +use chrono::{DateTime, Utc}; +use warp::Filter; + +use crate::database::Database; + +pub fn get_routes() -> impl Filter + Clone { + create_user().or(auth_user()).or(get_user()) +} + +#[derive(serde::Serialize, serde::Deserialize)] +struct UserCreateRequest { + username: String, + password: String, +} +#[derive(serde::Serialize, serde::Deserialize)] +struct UserCreateResponse { + success: bool, + message: String, +} +fn create_user() -> impl Filter + Clone { + warp::post() + .and(warp::path!("user" / "create")) + .and(warp::body::json()) + .map(|body: UserCreateRequest| { + let db = Database::new(); + let result = db.add_user(&body.username, &body.password); + let reply = if result.is_ok() { + UserCreateResponse { + success: true, + message: "".to_string(), + } + } else { + UserCreateResponse { + success: false, + message: format!("{:#?}", result.unwrap_err()), + } + }; + warp::reply::json(&reply) + }) +} + +#[derive(serde::Serialize, serde::Deserialize)] +struct UserGetResponse { + success: bool, + message: String, + creation_time: Option>, + premium: Option, +} +fn get_user() -> impl Filter + Clone { + warp::get() + .and(warp::path!("user" / String)) + .map(|username: String| { + let db = Database::new(); + let acc = db.get_user(&username); + let reply = if acc.is_err() { + UserGetResponse { + success: false, + message: format!("{}", acc.unwrap_err()), + creation_time: None, + premium: None, + } + } else { + let acc = acc.unwrap(); + UserGetResponse { + success: false, + message: "".to_string(), + creation_time: Some(acc.creation_time()), + premium: Some(acc.premium()), + } + }; + warp::reply::json(&reply) + }) +} + +#[derive(serde::Serialize, serde::Deserialize)] +struct UserAuthRequest { + username: String, + password: String, +} +#[derive(serde::Serialize, serde::Deserialize)] +struct UserAuthResponse { + success: bool, + message: String, +} +fn auth_user() -> impl Filter + Clone { + warp::post() + .and(warp::path!("user" / "auth")) + .and(warp::body::json()) + .map(|body: UserAuthRequest| { + let db = Database::new(); + let reply = if db.check_login(&body.username, &body.password) { + UserAuthResponse { + success: true, + message: "".to_string(), + } + } else { + UserAuthResponse { + success: false, + message: "Username or Password is invalid".to_string(), + } + }; + warp::reply::json(&reply) + }) +} diff --git a/user_db.db3 b/user_db.db3 new file mode 100644 index 0000000..9eedc79 Binary files /dev/null and b/user_db.db3 differ