some work

This commit is contained in:
c0repwn3r 2023-03-26 11:08:06 -04:00
parent 109afa50ab
commit c591c63f40
Signed by: core
GPG Key ID: FDBF740DADDCEECF
14 changed files with 233 additions and 36 deletions

2
.env
View File

@ -1 +1 @@
DATABASE_URL=postgres://postgres:postgres@localhost/hotel DATABASE_URL=postgres://postgres@localhost/hotel

View File

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="DataSourceManagerImpl" format="xml" multifile-model="true"> <component name="DataSourceManagerImpl" format="xml" multifile-model="true">
<data-source source="LOCAL" name="hotel@localhost" uuid="105f1b61-47ca-4e1f-868e-283fceb34f96"> <data-source source="LOCAL" name="hotel@localhost" uuid="fcbea912-4e4c-4fa0-90f4-86901bab7984">
<driver-ref>postgresql</driver-ref> <driver-ref>postgresql</driver-ref>
<synchronize>true</synchronize> <synchronize>true</synchronize>
<jdbc-driver>org.postgresql.Driver</jdbc-driver> <jdbc-driver>org.postgresql.Driver</jdbc-driver>

8
Cargo.lock generated
View File

@ -581,6 +581,12 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "hex"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]] [[package]]
name = "hotel" name = "hotel"
version = "0.1.0" version = "0.1.0"
@ -589,9 +595,11 @@ dependencies = [
"diesel", "diesel",
"diesel_migrations", "diesel_migrations",
"dotenvy", "dotenvy",
"hex",
"log", "log",
"once_cell", "once_cell",
"r2d2", "r2d2",
"rand",
"serde", "serde",
"simple_logger", "simple_logger",
"toml 0.7.3", "toml 0.7.3",

View File

@ -20,3 +20,6 @@ diesel = { version = "2.0.0", features = ["postgres", "r2d2"] } # Database
dotenvy = "0.15" # Database dotenvy = "0.15" # Database
diesel_migrations = "2.0.0" # Database diesel_migrations = "2.0.0" # Database
r2d2 = "0.8.10" # Database r2d2 = "0.8.10" # Database
rand = "0.8.5" # Misc.
hex = "0.4.3" # Misc.

View File

@ -1,5 +1,6 @@
CREATE TABLE users ( CREATE TABLE users (
id SERIAL NOT NULL PRIMARY KEY, id SERIAL NOT NULL PRIMARY KEY,
name VARCHAR(128) NOT NULL UNIQUE, name VARCHAR(128) NOT NULL UNIQUE,
discord_id BIGINT NOT NULL UNIQUE discord_id BIGINT NOT NULL UNIQUE,
password_hash VARCHAR(256) NOT NULL
); );

View File

@ -12,20 +12,20 @@ pub static CONFIG: Lazy<HotelConfig> = Lazy::new(|| {
} }
}; };
let config = match toml::from_str(&config_str) { match toml::from_str(&config_str) {
Ok(cfg) => cfg, Ok(cfg) => cfg,
Err(e) => { Err(e) => {
error!("Unable to parse config file: {}", e); error!("Unable to parse config file: {}", e);
std::process::exit(1); std::process::exit(1);
} }
}; }
config
}); });
#[derive(Serialize, Debug, Deserialize)] #[derive(Serialize, Debug, Deserialize)]
pub struct HotelConfig { pub struct HotelConfig {
pub db_uri: String, pub db_uri: String,
pub authorized_3fa_tokens: Vec<String> pub authorized_3fa_tokens: Vec<String>,
pub mfa_codes_expire_in: i64,
pub authorized_new_user_tokens: Vec<String>
} }

View File

@ -13,40 +13,18 @@ pub mod config;
pub mod error; pub mod error;
pub mod models; pub mod models;
pub mod schema; pub mod schema;
pub mod routes;
pub mod util;
pub const MIGRATIONS: EmbeddedMigrations = embed_migrations!(); 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<CodeRequest3FA>) -> 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<ConnectionManager<PgConnection>>; pub type PgPool = Pool<ConnectionManager<PgConnection>>;
#[actix_web::main] #[actix_web::main]
async fn main() -> std::io::Result<()> { async fn main() -> std::io::Result<()> {
simple_logger::init_with_level(Level::Debug).unwrap(); 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 manager = ConnectionManager::new(&CONFIG.db_uri);
let pool: PgPool = match Pool::builder().build(manager) { let pool: PgPool = match Pool::builder().build(manager) {
@ -91,7 +69,8 @@ async fn main() -> std::io::Result<()> {
}) })
).into() ).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))? .bind(("127.0.0.1", 8080))?
.run() .run()

View File

@ -1,11 +1,21 @@
use diesel::prelude::*; use diesel::prelude::*;
use crate::schema::{users, codes_3fa};
#[derive(Queryable)] #[derive(Queryable)]
#[diesel(table_name = users)] #[diesel(table_name = users)]
pub struct User { pub struct User {
pub id: i32, pub id: i32,
pub name: String, 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)] #[derive(Queryable)]
@ -17,3 +27,11 @@ pub struct Code3FA {
pub user_id: i32, pub user_id: i32,
pub expires_on: i64 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
}

1
src/routes/mod.rs Normal file
View File

@ -0,0 +1 @@
pub mod v1;

141
src/routes/v1/code_3fa.rs Normal file
View File

@ -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<PgPool>, req: Json<CodeRequest3FA>) -> 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::<User>(&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<Code3FA> = 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)
}

2
src/routes/v1/mod.rs Normal file
View File

@ -0,0 +1,2 @@
pub mod code_3fa;
pub mod user_add;

31
src/routes/v1/user_add.rs Normal file
View File

@ -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<PgPool>, req: Json<UserAddRequest>) -> 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")
}

View File

@ -14,6 +14,7 @@ diesel::table! {
id -> Int4, id -> Int4,
name -> Varchar, name -> Varchar,
discord_id -> Int8, discord_id -> Int8,
password_hash -> Varchar,
} }
} }

12
src/util.rs Normal file
View File

@ -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)
}