diff --git a/Cargo.lock b/Cargo.lock index 8f21323..262ab33 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -807,9 +807,9 @@ checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" [[package]] name = "j_db" -version = "0.1.0" +version = "0.1.2" source = "registry+https://git.jojodev.com/joeyahines/_cargo-index.git" -checksum = "1243e6cadfd7e022fed3e9bc1f1465e84da7fb22bc149d8fddea3ccac07ce0b0" +checksum = "ae76696901f0cd9f850fe6608f4ce640c055444a2f453382fb7deda45a141b75" dependencies = [ "byteorder", "json", @@ -1148,17 +1148,21 @@ dependencies = [ "async-trait", "axum", "axum-macros", + "base64", + "bitflags 2.4.2", "chrono", "chrono-tz", "config", "env_logger", "hex", "j_db", + "json", "log", "multer", "rand", "serde", "serde_json", + "sha2", "structopt", "tokio", "tracing-subscriber", diff --git a/Cargo.toml b/Cargo.toml index 889417a..fbdc7df 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,7 +6,7 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -j_db = { version = "0.1.0", registry = "jojo-dev" } +j_db = { version = "0.1.2", registry = "jojo-dev" } axum-macros = "0.4.1" serde = "1.0.195" config = "0.14.0" @@ -22,6 +22,10 @@ env_logger = "0.11.0" multer = "3.0.0" serde_json = "1.0.111" rand = "0.8.5" +sha2 = "0.10.8" +bitflags = "2.4.2" +json = "0.12.4" +base64 = "0.21.7" [dependencies.axum] version = "0.7.4" diff --git a/src/main.rs b/src/main.rs index 4ea137c..87ebf79 100644 --- a/src/main.rs +++ b/src/main.rs @@ -24,14 +24,46 @@ use rand::thread_rng; use serde::{Deserialize, Serialize}; use std::path::PathBuf; use std::sync::Arc; +use base64::Engine; use structopt::StructOpt; use tokio::sync::RwLock; +use crate::model::api_key::{ApiKey, ApiPermissions}; type PicContext = Arc; + +#[derive(StructOpt, Debug, Clone)] +#[structopt(about = "PicOx Commands")] +enum SubCommands { + /// Start the PicOx server + Start, + /// Create a Picox API Key + CreateKey { + /// Key description + description: String, + /// API Key permissions (WRITE, DELETE) + permissions: ApiPermissions + }, + /// Dump the database state to json + Dump { + /// Output path + out_path: PathBuf, + }, + /// Import the database state from json + Import { + /// Path to json containing DB state + db_file: PathBuf + } +} + + #[derive(Debug, Clone, StructOpt)] struct Args { + /// Path to the config file + #[structopt(short, long, env = "PICOX_CONFIG")] pub config: PathBuf, + #[structopt(subcommand)] + pub command: SubCommands } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -275,14 +307,7 @@ async fn query_album( Ok(Response(resp)) } -#[tokio::main] -async fn main() { - let args = Args::from_args(); - - let config = PicOxConfig::new(args.config); - - let db = Database::new(&config.db_path).unwrap(); - +async fn run_picox(db: Database, config: PicOxConfig) { let store_manager = StorageManager::new(config.storage_config.clone()); let context = Context { @@ -293,9 +318,6 @@ async fn main() { let context = Arc::new(context); - // initialize tracing - tracing_subscriber::fmt::init(); - // build our application with a route let app = Router::new() // `GET /` goes to `root` .route("/api/album/create", post(create_album)) @@ -306,8 +328,47 @@ async fn main() { .route("/api/image/", get(query_images)) .with_state(context); - // run our app with hyper, listening globally on port 3000 let listener = tokio::net::TcpListener::bind(&config.host).await.unwrap(); info!("Serving at {}", config.host); axum::serve(listener, app).await.unwrap(); } + +#[tokio::main] +async fn main() { + let args = Args::from_args(); + + // initialize tracing + tracing_subscriber::fmt::init(); + + let config = PicOxConfig::new(args.config); + + let db = Database::new(&config.db_path).unwrap(); + + match args.command { + SubCommands::Start => { + run_picox(db, config).await; + } + SubCommands::CreateKey { description, permissions } => { + let (token, api_key) = ApiKey::create_new_key(&db, description, permissions).unwrap(); + + info!("New Key info: {:?}", api_key); + info!("Token: {}", base64::prelude::BASE64_STANDARD.encode(token)); + db.db.flush().unwrap(); + } + SubCommands::Dump {out_path} => { + info!("Dumping database state to {:?}", out_path); + + tokio::fs::write(out_path, db.dump_db().unwrap().pretty(4)).await.unwrap(); + } + SubCommands::Import { db_file } => { + info!("Importing database state from {:?}", db_file); + let db_state = tokio::fs::read(db_file).await.unwrap(); + let db_state = String::from_utf8(db_state).unwrap(); + let db_state = json::parse(&db_state).unwrap(); + + db.import_db(db_state).unwrap(); + db.db.flush().unwrap(); + } + } + +} diff --git a/src/model/api_key.rs b/src/model/api_key.rs new file mode 100644 index 0000000..87355fd --- /dev/null +++ b/src/model/api_key.rs @@ -0,0 +1,67 @@ +use std::str::FromStr; +use base64::Engine; +use j_db::database::Database; +use serde::{Deserialize, Serialize}; + +use bitflags::bitflags; +use rand::RngCore; +use sha2::Digest; + +bitflags! { + #[derive(Serialize, Deserialize, Clone, Debug)] + pub struct ApiPermissions: u32 { + const WRITE = 0b00000001; + const DELETE = 0b00000010; + } +} + +impl FromStr for ApiPermissions { + type Err = bitflags::parser::ParseError; + + fn from_str(s: &str) -> Result { + bitflags::parser::from_str(s) + } +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct ApiKey { + pub token_hash: String, + pub description: String, + pub permissions: ApiPermissions, + + id: Option +} + +impl ApiKey { + pub fn create_new_key(db: &Database, description: String, permissions: ApiPermissions) -> Result<(Vec, ApiKey), j_db::error::JDbError>{ + let mut key = [0u8; 32]; + rand::thread_rng().fill_bytes(&mut key); + + let hash = sha2::Sha256::digest(key); + + let api_key = ApiKey { + token_hash: base64::prelude::BASE64_STANDARD.encode(&hash), + description, + permissions, + id: None, + }; + + let api_key = db.insert(api_key)?; + + Ok((key.to_vec(), api_key)) + } +} + +impl j_db::model::JdbModel for ApiKey { + fn id(&self) -> Option { + self.id + } + + fn set_id(&mut self, id: u64) { + self.id = Some(id); + } + + fn tree() -> String { + "ApiKey".to_string() + } +} \ No newline at end of file diff --git a/src/model/mod.rs b/src/model/mod.rs index 2519aa0..8ada03b 100644 --- a/src/model/mod.rs +++ b/src/model/mod.rs @@ -1,2 +1,3 @@ pub mod album; pub mod image; +pub mod api_key;