use std::str::FromStr; use anyhow::{Context, anyhow}; use komodo_client::entities::{ action::Action, alert::Alert, alerter::Alerter, api_key::ApiKey, build::Build, builder::Builder, config::DatabaseConfig, deployment::Deployment, onboarding_key::OnboardingKey, permission::Permission, procedure::Procedure, provider::{DockerRegistryAccount, GitProviderAccount}, repo::Repo, server::Server, stack::Stack, stats::SystemStatsRecord, swarm::Swarm, sync::ResourceSync, tag::Tag, update::Update, user::{User, UserConfig}, user_group::UserGroup, variable::Variable, }; use mongo_indexed::{create_index, create_unique_index}; use mungos::{ init::MongoBuilder, mongodb::{ Collection, Database, bson::{doc, oid::ObjectId}, }, }; pub use mongo_indexed; pub use mungos; pub use mungos::mongodb::bson; pub mod utils; #[derive(Debug)] pub struct Client { pub users: Collection, pub user_groups: Collection, pub permissions: Collection, pub api_keys: Collection, pub onboarding_keys: Collection, pub tags: Collection, pub variables: Collection, pub git_accounts: Collection, pub registry_accounts: Collection, pub updates: Collection, pub alerts: Collection, pub stats: Collection, // RESOURCES pub swarms: Collection, pub servers: Collection, pub deployments: Collection, pub builds: Collection, pub builders: Collection, pub repos: Collection, pub procedures: Collection, pub actions: Collection, pub alerters: Collection, pub resource_syncs: Collection, pub stacks: Collection, // pub db: Database, } impl Client { pub async fn new( config: &DatabaseConfig, ) -> anyhow::Result { let db = init(config).await?; Self::from_database(db).await } pub async fn from_database(db: Database) -> anyhow::Result { let client = Client { users: mongo_indexed::collection(&db, true).await?, user_groups: mongo_indexed::collection(&db, true).await?, permissions: mongo_indexed::collection(&db, true).await?, api_keys: mongo_indexed::collection(&db, true).await?, onboarding_keys: mongo_indexed::collection(&db, true).await?, tags: mongo_indexed::collection(&db, true).await?, variables: mongo_indexed::collection(&db, true).await?, git_accounts: mongo_indexed::collection(&db, true).await?, registry_accounts: mongo_indexed::collection(&db, true).await?, updates: mongo_indexed::collection(&db, true).await?, alerts: mongo_indexed::collection(&db, true).await?, stats: mongo_indexed::collection(&db, true).await?, // RESOURCES swarms: resource_collection(&db, "Swarm").await?, servers: resource_collection(&db, "Server").await?, deployments: resource_collection(&db, "Deployment").await?, builds: resource_collection(&db, "Build").await?, builders: resource_collection(&db, "Builder").await?, repos: resource_collection(&db, "Repo").await?, alerters: resource_collection(&db, "Alerter").await?, procedures: resource_collection(&db, "Procedure").await?, actions: resource_collection(&db, "Action").await?, resource_syncs: resource_collection(&db, "ResourceSync") .await?, stacks: resource_collection(&db, "Stack").await?, // db, }; Ok(client) } /// Updates a user's password using a DB call. pub async fn set_user_password( &self, user: &User, password: &str, ) -> anyhow::Result<()> { let UserConfig::Local { .. } = user.config else { return Err(anyhow!( "User is not a 'Local' (username / password) user" )); }; if password.is_empty() { return Err(anyhow!("Password cannot be empty.")); } let id = ObjectId::from_str(&user.id) .context("User id not valid ObjectId.")?; let hashed_password = hash_password(password).context("Failed to hash password")?; self .users .update_one( doc! { "_id": id }, doc! { "$set": { "config.data.password": hashed_password } }, ) .await .context("Failed to update user password on database.")?; Ok(()) } } /// Initializes unindexed database handle. pub async fn init( DatabaseConfig { uri, address, username, password, app_name, db_name, }: &DatabaseConfig, ) -> anyhow::Result { let mut client = MongoBuilder::default().app_name(app_name); match ( !uri.is_empty(), !address.is_empty(), !username.is_empty(), !password.is_empty(), ) { (true, _, _, _) => { client = client.uri(uri); } (_, true, true, true) => { client = client .address(address) .username(username) .password(password); } (_, true, _, _) => { client = client.address(address); } _ => { return Err(anyhow!( "'config.database' not configured correctly. must pass either 'config.database.uri', or 'config.database.address' + 'config.database.username' + 'config.database.password'" )); } } let client = client .build() .await .context("Failed to initialize database connection.")?; Ok(client.database(db_name)) } async fn resource_collection( db: &Database, collection_name: &str, ) -> anyhow::Result> { let coll = db.collection::(collection_name); create_unique_index(&coll, "name").await?; create_index(&coll, "tags").await?; Ok(coll) } const BCRYPT_COST: u32 = 10; pub fn hash_password

(password: P) -> anyhow::Result where P: AsRef<[u8]>, { bcrypt::hash(password, BCRYPT_COST) .context("failed to hash password") }