Added CLI interface and api keys

+ API is not locked down yet though
main
Joey Hines 2024-02-06 21:24:10 -07:00
parent 13d0c9401e
commit b9b14a7118
Signed by: joeyahines
GPG Key ID: 995E531F7A569DDB
5 changed files with 152 additions and 15 deletions

8
Cargo.lock generated
View File

@ -807,9 +807,9 @@ checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c"
[[package]] [[package]]
name = "j_db" name = "j_db"
version = "0.1.0" version = "0.1.2"
source = "registry+https://git.jojodev.com/joeyahines/_cargo-index.git" source = "registry+https://git.jojodev.com/joeyahines/_cargo-index.git"
checksum = "1243e6cadfd7e022fed3e9bc1f1465e84da7fb22bc149d8fddea3ccac07ce0b0" checksum = "ae76696901f0cd9f850fe6608f4ce640c055444a2f453382fb7deda45a141b75"
dependencies = [ dependencies = [
"byteorder", "byteorder",
"json", "json",
@ -1148,17 +1148,21 @@ dependencies = [
"async-trait", "async-trait",
"axum", "axum",
"axum-macros", "axum-macros",
"base64",
"bitflags 2.4.2",
"chrono", "chrono",
"chrono-tz", "chrono-tz",
"config", "config",
"env_logger", "env_logger",
"hex", "hex",
"j_db", "j_db",
"json",
"log", "log",
"multer", "multer",
"rand", "rand",
"serde", "serde",
"serde_json", "serde_json",
"sha2",
"structopt", "structopt",
"tokio", "tokio",
"tracing-subscriber", "tracing-subscriber",

View File

@ -6,7 +6,7 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
j_db = { version = "0.1.0", registry = "jojo-dev" } j_db = { version = "0.1.2", registry = "jojo-dev" }
axum-macros = "0.4.1" axum-macros = "0.4.1"
serde = "1.0.195" serde = "1.0.195"
config = "0.14.0" config = "0.14.0"
@ -22,6 +22,10 @@ env_logger = "0.11.0"
multer = "3.0.0" multer = "3.0.0"
serde_json = "1.0.111" serde_json = "1.0.111"
rand = "0.8.5" rand = "0.8.5"
sha2 = "0.10.8"
bitflags = "2.4.2"
json = "0.12.4"
base64 = "0.21.7"
[dependencies.axum] [dependencies.axum]
version = "0.7.4" version = "0.7.4"

View File

@ -24,14 +24,46 @@ use rand::thread_rng;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::path::PathBuf; use std::path::PathBuf;
use std::sync::Arc; use std::sync::Arc;
use base64::Engine;
use structopt::StructOpt; use structopt::StructOpt;
use tokio::sync::RwLock; use tokio::sync::RwLock;
use crate::model::api_key::{ApiKey, ApiPermissions};
type PicContext = Arc<Context>; type PicContext = Arc<Context>;
#[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)] #[derive(Debug, Clone, StructOpt)]
struct Args { struct Args {
/// Path to the config file
#[structopt(short, long, env = "PICOX_CONFIG")]
pub config: PathBuf, pub config: PathBuf,
#[structopt(subcommand)]
pub command: SubCommands
} }
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
@ -275,14 +307,7 @@ async fn query_album(
Ok(Response(resp)) Ok(Response(resp))
} }
#[tokio::main] async fn run_picox(db: Database, config: PicOxConfig) {
async fn main() {
let args = Args::from_args();
let config = PicOxConfig::new(args.config);
let db = Database::new(&config.db_path).unwrap();
let store_manager = StorageManager::new(config.storage_config.clone()); let store_manager = StorageManager::new(config.storage_config.clone());
let context = Context { let context = Context {
@ -293,9 +318,6 @@ async fn main() {
let context = Arc::new(context); let context = Arc::new(context);
// initialize tracing
tracing_subscriber::fmt::init();
// build our application with a route
let app = Router::new() let app = Router::new()
// `GET /` goes to `root` // `GET /` goes to `root`
.route("/api/album/create", post(create_album)) .route("/api/album/create", post(create_album))
@ -306,8 +328,47 @@ async fn main() {
.route("/api/image/", get(query_images)) .route("/api/image/", get(query_images))
.with_state(context); .with_state(context);
// run our app with hyper, listening globally on port 3000
let listener = tokio::net::TcpListener::bind(&config.host).await.unwrap(); let listener = tokio::net::TcpListener::bind(&config.host).await.unwrap();
info!("Serving at {}", config.host); info!("Serving at {}", config.host);
axum::serve(listener, app).await.unwrap(); 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();
}
}
}

View File

@ -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<Self, Self::Err> {
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<u64>
}
impl ApiKey {
pub fn create_new_key(db: &Database, description: String, permissions: ApiPermissions) -> Result<(Vec<u8>, 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<u64> {
self.id
}
fn set_id(&mut self, id: u64) {
self.id = Some(id);
}
fn tree() -> String {
"ApiKey".to_string()
}
}

View File

@ -1,2 +1,3 @@
pub mod album; pub mod album;
pub mod image; pub mod image;
pub mod api_key;