diff --git a/.env b/.env index ebec27a..03fe2db 100644 --- a/.env +++ b/.env @@ -1 +1 @@ -DATABASE_URL=postgres://postgres:postgres@localhost/hotel +DATABASE_URL=postgres://postgres@localhost/hotel diff --git a/.idea/dataSources.xml b/.idea/dataSources.xml index a03d898..ed3484f 100644 --- a/.idea/dataSources.xml +++ b/.idea/dataSources.xml @@ -1,7 +1,7 @@ - + postgresql true org.postgresql.Driver diff --git a/Cargo.lock b/Cargo.lock index 1bc0094..4659e4f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -581,6 +581,12 @@ dependencies = [ "libc", ] +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + [[package]] name = "hotel" version = "0.1.0" @@ -589,9 +595,11 @@ dependencies = [ "diesel", "diesel_migrations", "dotenvy", + "hex", "log", "once_cell", "r2d2", + "rand", "serde", "simple_logger", "toml 0.7.3", diff --git a/Cargo.toml b/Cargo.toml index 1f06a5a..6fe3296 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,4 +19,7 @@ simple_logger = "4.1.0" # Logging diesel = { version = "2.0.0", features = ["postgres", "r2d2"] } # Database dotenvy = "0.15" # Database diesel_migrations = "2.0.0" # Database -r2d2 = "0.8.10" # Database \ No newline at end of file +r2d2 = "0.8.10" # Database + +rand = "0.8.5" # Misc. +hex = "0.4.3" # Misc. \ No newline at end of file diff --git a/migrations/2023-03-26-024321_create_users/up.sql b/migrations/2023-03-26-024321_create_users/up.sql index 5beeeb9..25cb6e8 100644 --- a/migrations/2023-03-26-024321_create_users/up.sql +++ b/migrations/2023-03-26-024321_create_users/up.sql @@ -1,5 +1,6 @@ CREATE TABLE users ( id SERIAL NOT NULL PRIMARY KEY, name VARCHAR(128) NOT NULL UNIQUE, - discord_id BIGINT NOT NULL UNIQUE + discord_id BIGINT NOT NULL UNIQUE, + password_hash VARCHAR(256) NOT NULL ); \ No newline at end of file diff --git a/src/config.rs b/src/config.rs index b7a10a1..239bc2e 100644 --- a/src/config.rs +++ b/src/config.rs @@ -12,20 +12,20 @@ pub static CONFIG: Lazy = Lazy::new(|| { } }; - let config = match toml::from_str(&config_str) { + match toml::from_str(&config_str) { Ok(cfg) => cfg, Err(e) => { error!("Unable to parse config file: {}", e); std::process::exit(1); } - }; - - config + } }); #[derive(Serialize, Debug, Deserialize)] pub struct HotelConfig { pub db_uri: String, - pub authorized_3fa_tokens: Vec + pub authorized_3fa_tokens: Vec, + pub mfa_codes_expire_in: i64, + pub authorized_new_user_tokens: Vec } diff --git a/src/main.rs b/src/main.rs index c53bec7..cbdee96 100644 --- a/src/main.rs +++ b/src/main.rs @@ -13,40 +13,18 @@ pub mod config; pub mod error; pub mod models; pub mod schema; +pub mod routes; +pub mod util; pub const MIGRATIONS: EmbeddedMigrations = embed_migrations!(); -#[derive(Serialize, Deserialize)] -pub struct CodeRequest3FA { - token: String, - user: String -} - -#[post("/v1/3fa_code")] -pub async fn get_3fa_code(req: Json) -> HttpResponse { - if !CONFIG.authorized_3fa_tokens.contains(&req.token) { - return HttpResponse::Unauthorized().json(APIErrorResponse { - errors: vec![ - APIError { - code: "ERR_INVALID_CODEREQ_TOKEN".to_string(), - message: "Invalid codereq token".to_string(), - } - ], - }) - } - - - - HttpResponse::Ok().body("d") -} - pub type PgPool = Pool>; #[actix_web::main] async fn main() -> std::io::Result<()> { simple_logger::init_with_level(Level::Debug).unwrap(); - info!("Connecting to database..."); + info!("Connecting to database at {}...", CONFIG.db_uri); let manager = ConnectionManager::new(&CONFIG.db_uri); let pool: PgPool = match Pool::builder().build(manager) { @@ -91,7 +69,8 @@ async fn main() -> std::io::Result<()> { }) ).into() })) - .service(get_3fa_code) + .service(routes::v1::code_3fa::get_3fa_code) + .service(routes::v1::user_add::add_user_request) }) .bind(("127.0.0.1", 8080))? .run() diff --git a/src/models.rs b/src/models.rs index 85cc434..96d4467 100644 --- a/src/models.rs +++ b/src/models.rs @@ -1,11 +1,21 @@ use diesel::prelude::*; +use crate::schema::{users, codes_3fa}; #[derive(Queryable)] #[diesel(table_name = users)] pub struct User { pub id: i32, pub name: String, - pub discord_id: i64 + pub discord_id: i64, + pub password_hash: String +} + +#[derive(Insertable)] +#[diesel(table_name = users)] +pub struct NewUser { + pub name: String, + pub discord_id: i64, + pub password_hash: String } #[derive(Queryable)] @@ -16,4 +26,12 @@ pub struct Code3FA { pub code: String, pub user_id: i32, pub expires_on: i64 +} + +#[derive(Insertable, Clone)] +#[diesel(table_name = codes_3fa)] +pub struct NewCode3FA { + pub code: String, + pub user_id: i32, + pub expires_on: i64 } \ No newline at end of file diff --git a/src/routes/mod.rs b/src/routes/mod.rs new file mode 100644 index 0000000..5dd9fd0 --- /dev/null +++ b/src/routes/mod.rs @@ -0,0 +1 @@ +pub mod v1; \ No newline at end of file diff --git a/src/routes/v1/code_3fa.rs b/src/routes/v1/code_3fa.rs new file mode 100644 index 0000000..95e6848 --- /dev/null +++ b/src/routes/v1/code_3fa.rs @@ -0,0 +1,141 @@ +use actix_web::{HttpResponse, web}; +use actix_web::web::{Data, Json}; +use crate::config::CONFIG; +use crate::error::{APIError, APIErrorResponse}; +use actix_web::post; +use diesel::prelude::*; +use log::error; +use rand::Rng; +use serde::{Serialize, Deserialize}; +use crate::models::{Code3FA, NewCode3FA, User}; +use crate::PgPool; +use crate::util::current_unix_time; + +#[derive(Serialize, Deserialize, Clone)] +pub struct CodeRequest3FA { + token: String, + user: String +} + +#[derive(Serialize, Deserialize)] +pub struct CodeResponse { + user: String, + code: String, + id: i32, + valid_until: i64 +} + +#[post("/v1/3fa_code")] +pub async fn get_3fa_code(pool: Data, req: Json) -> HttpResponse { + use crate::schema::users; + use crate::schema::codes_3fa; + + if !CONFIG.authorized_3fa_tokens.contains(&req.token) { + return HttpResponse::Unauthorized().json(APIErrorResponse { + errors: vec![ + APIError { + code: "ERR_INVALID_CODEREQ_TOKEN".to_string(), + message: "Invalid codereq token".to_string(), + } + ], + }) + } + + let req_clone = req.clone(); + let pool_clone = pool.clone(); + let results = match web::block(move || { + let mut conn = pool_clone.get().expect("Unable to get db pool"); + users::table.filter(users::name.eq(&req_clone.user)).load::(&mut conn) + }).await { + Ok(r) => r, + Err(e) => { + error!("Database error: {}", e); + return HttpResponse::InternalServerError().json(APIErrorResponse { + errors: vec![ + APIError { + code: "ERR_BLOCKING_ERROR".to_string(), + message: "There was an error running the database request. Please try again later.".to_string() + } + ] + }) + } + }; + let user_list = match results { + Ok(r) => r, + Err(e) => { + error!("Database error: {}", e); + return HttpResponse::InternalServerError().json(APIErrorResponse { + errors: vec![ + APIError { + code: "ERR_DB_ERROR".to_string(), + message: "There was an error fetching the user. Please try again later.".to_string() + } + ] + }) + } + }; + if user_list.is_empty() { + return HttpResponse::Unauthorized().json(APIErrorResponse { + errors: vec![ + APIError { + code: "ERR_USER_DOES_NOT_EXIST".to_string(), + message: "Cannot issue codereq for a non-existent user".to_string() + } + ] + }) + } + let user = &user_list[0]; + + let codereq = NewCode3FA { + code: random_3fa_code(), + user_id: user.id, + expires_on: current_unix_time() + CONFIG.mfa_codes_expire_in, + }; + + let codereq_clone = codereq.clone(); + let insert_result: QueryResult = match web::block(move || { + let mut conn = pool.get().expect("Unable to get db pool"); + diesel::insert_into(codes_3fa::table).values(codereq_clone).get_result(&mut conn) + }).await { + Ok(r) => r, + Err(e) => { + error!("Database error: {}", e); + return HttpResponse::InternalServerError().json(APIErrorResponse { + errors: vec![ + APIError { + code: "ERR_BLOCKING_ERROR".to_string(), + message: "There was an error running the insert database request. Please try again later.".to_string() + } + ] + }) + } + }; + let code = match insert_result { + Ok(r) => r, + Err(e) => { + error!("Database error: {}", e); + return HttpResponse::InternalServerError().json(APIErrorResponse { + errors: vec![ + APIError { + code: "ERR_DB_ERROR".to_string(), + message: "There was an error creating the codereq. Please try again later.".to_string() + } + ] + }) + } + }; + + HttpResponse::Ok().json(CodeResponse { + user: req.user.clone(), + code: code.code, + id: code.id, + valid_until: code.expires_on, + }) +} + +fn random_3fa_code() -> String { + // 3fa codes are 10-digit hex values (5-byte) + let mut rng = rand::thread_rng(); + let bytes: [u8; 5] = rng.gen(); + hex::encode(bytes) +} \ No newline at end of file diff --git a/src/routes/v1/mod.rs b/src/routes/v1/mod.rs new file mode 100644 index 0000000..6dbeeae --- /dev/null +++ b/src/routes/v1/mod.rs @@ -0,0 +1,2 @@ +pub mod code_3fa; +pub mod user_add; \ No newline at end of file diff --git a/src/routes/v1/user_add.rs b/src/routes/v1/user_add.rs new file mode 100644 index 0000000..1c2192a --- /dev/null +++ b/src/routes/v1/user_add.rs @@ -0,0 +1,31 @@ +use actix_web::HttpResponse; +use actix_web::post; +use actix_web::web::{Data, Json}; +use serde::{Serialize, Deserialize}; +use crate::config::CONFIG; +use crate::error::{APIError, APIErrorResponse}; +use crate::PgPool; + +#[derive(Serialize, Deserialize, Clone)] +pub struct UserAddRequest { + pub token: String, + pub name: String, + pub discord_id: i64, + pub password_hash: String +} + +#[post("/v1/user/add")] +pub async fn add_user_request(db: Data, req: Json) -> HttpResponse { + if !CONFIG.authorized_new_user_tokens.contains(&req.token) { + return HttpResponse::Unauthorized().json(APIErrorResponse { + errors: vec![ + APIError { + code: "ERR_INVALID_NEW_USER_TOKEN".to_string(), + message: "Invalid newuser token".to_string(), + } + ], + }) + } + + HttpResponse::Ok().body("d") +} \ No newline at end of file diff --git a/src/schema.rs b/src/schema.rs index d8ee678..f30acbd 100644 --- a/src/schema.rs +++ b/src/schema.rs @@ -14,6 +14,7 @@ diesel::table! { id -> Int4, name -> Varchar, discord_id -> Int8, + password_hash -> Varchar, } } diff --git a/src/util.rs b/src/util.rs new file mode 100644 index 0000000..b9d1df8 --- /dev/null +++ b/src/util.rs @@ -0,0 +1,12 @@ +use std::ops::Add; +use std::time::{Duration, SystemTime, UNIX_EPOCH}; + +pub fn current_unix_time() -> i64 { + let time = SystemTime::now().duration_since(UNIX_EPOCH).expect("Time went backwards").as_secs(); + time as i64 +} + +pub fn has_expired(time: i64) -> bool { + let time = SystemTime::UNIX_EPOCH.add(Duration::from_secs(time as u64)); + SystemTime::now().lt(&time) +} \ No newline at end of file