diff options
| -rw-r--r-- | Cargo.lock | 92 | ||||
| -rw-r--r-- | Cargo.toml | 2 | ||||
| -rw-r--r-- | lib/auth-service/Cargo.toml | 3 | ||||
| -rw-r--r-- | lib/auth-service/src/client/mod.rs | 49 | ||||
| -rw-r--r-- | lib/auth-service/src/lib.rs | 23 | ||||
| -rw-r--r-- | sellershut/Cargo.toml | 3 | ||||
| -rw-r--r-- | sellershut/sellershut.toml | 3 | ||||
| -rw-r--r-- | sellershut/src/config/cli/database.rs | 14 | ||||
| -rw-r--r-- | sellershut/src/config/cli/mod.rs | 39 | ||||
| -rw-r--r-- | sellershut/src/config/cli/oauth/discord.rs | 36 | ||||
| -rw-r--r-- | sellershut/src/config/cli/oauth/mod.rs | 17 | ||||
| -rw-r--r-- | sellershut/src/config/cli/validator.rs | 0 | ||||
| -rw-r--r-- | sellershut/src/config/mod.rs | 99 | ||||
| -rw-r--r-- | sellershut/src/main.rs | 12 | ||||
| -rw-r--r-- | sellershut/src/state/mod.rs | 5 |
15 files changed, 379 insertions, 18 deletions
@@ -87,7 +87,10 @@ name = "auth-service" version = "0.1.0" dependencies = [ "oauth2", + "secrecy", "thiserror 2.0.18", + "tracing", + "url", ] [[package]] @@ -334,6 +337,12 @@ dependencies = [ ] [[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] name = "errno" version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -430,6 +439,12 @@ dependencies = [ ] [[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + +[[package]] name = "heck" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -669,6 +684,16 @@ dependencies = [ ] [[package]] +name = "indexmap" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] name = "ipnet" version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1133,6 +1158,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a50f4cf475b65d88e057964e0e9bb1f0aa9bbb2036dc65c64596b42932536984" [[package]] +name = "secrecy" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e891af845473308773346dc847b2c23ee78fe442e0472ac50e22a18a93d3ae5a" +dependencies = [ + "serde", + "zeroize", +] + +[[package]] name = "sellershut" version = "0.1.0" dependencies = [ @@ -1140,11 +1175,14 @@ dependencies = [ "auth-service", "axum", "clap", + "secrecy", "serde", "tokio", + "toml", "tracing", "tracing-appender", "tracing-subscriber", + "url", ] [[package]] @@ -1202,6 +1240,15 @@ dependencies = [ ] [[package]] +name = "serde_spanned" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8bbf91e5a4d6315eee45e704372590b30e260ee83af6639d64557f51b067776" +dependencies = [ + "serde_core", +] + +[[package]] name = "serde_urlencoded" version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1463,6 +1510,45 @@ dependencies = [ ] [[package]] +name = "toml" +version = "0.9.11+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3afc9a848309fe1aaffaed6e1546a7a14de1f935dc9d89d32afd9a44bab7c46" +dependencies = [ + "indexmap", + "serde_core", + "serde_spanned", + "toml_datetime", + "toml_parser", + "toml_writer", + "winnow", +] + +[[package]] +name = "toml_datetime" +version = "0.7.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_parser" +version = "1.0.6+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3198b4b0a8e11f09dd03e133c0280504d0801269e9afa46362ffde1cbeebf44" +dependencies = [ + "winnow", +] + +[[package]] +name = "toml_writer" +version = "1.0.6+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607" + +[[package]] name = "tower" version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1971,6 +2057,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" [[package]] +name = "winnow" +version = "0.7.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" + +[[package]] name = "wit-bindgen" version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -8,7 +8,9 @@ readme = "README.md" documentation = "https://books.kanjala.com/sellershut" [workspace.dependencies] +secrecy = "0.10.3" serde = "1.0.228" thiserror = "2.0.18" tokio = "1.49.0" tracing = "0.1.44" +url = "2.5.8" diff --git a/lib/auth-service/Cargo.toml b/lib/auth-service/Cargo.toml index 147c2fa..8efdc57 100644 --- a/lib/auth-service/Cargo.toml +++ b/lib/auth-service/Cargo.toml @@ -7,5 +7,8 @@ readme.workspace = true documentation.workspace = true [dependencies] +secrecy = "0.10.3" oauth2 = "5.0.0" thiserror.workspace = true +tracing.workspace = true +url = { workspace = true, features = ["serde"] } diff --git a/lib/auth-service/src/client/mod.rs b/lib/auth-service/src/client/mod.rs new file mode 100644 index 0000000..25cf16c --- /dev/null +++ b/lib/auth-service/src/client/mod.rs @@ -0,0 +1,49 @@ +use oauth2::{AuthUrl, ClientId, ClientSecret, EndpointNotSet, EndpointSet, RedirectUrl, TokenUrl}; +use secrecy::{ExposeSecret, SecretString}; +use tracing::debug; +use url::Url; + +use crate::AuthServiceError; + +pub struct OauthClient( + oauth2::basic::BasicClient< + EndpointSet, + EndpointNotSet, + EndpointNotSet, + EndpointNotSet, + EndpointSet, + >, +); + +pub struct ClientConfig { + client_id: String, + client_secret: SecretString, + token_url: Url, + auth_url: Url, +} + +impl TryFrom<ClientConfig> for OauthClient { + type Error = AuthServiceError; + + fn try_from(value: ClientConfig) -> Result<Self, Self::Error> { + debug!("creating oauth client"); + Ok(Self( + oauth2::basic::BasicClient::new(ClientId::new(value.client_id)) + .set_client_secret(ClientSecret::new( + value.client_secret.expose_secret().to_string(), + )) + .set_auth_uri(AuthUrl::from_url(value.auth_url)) + .set_token_uri(TokenUrl::from_url(value.token_url)), + )) + } +} + +impl OauthClient { + #[must_use] + pub fn with_redirect_url(self, url: &Url) -> Self { + Self( + self.0 + .set_redirect_uri(RedirectUrl::from_url(url.to_owned())), + ) + } +} diff --git a/lib/auth-service/src/lib.rs b/lib/auth-service/src/lib.rs index b93cf3f..f7b9e80 100644 --- a/lib/auth-service/src/lib.rs +++ b/lib/auth-service/src/lib.rs @@ -1,14 +1,15 @@ -pub fn add(left: u64, right: u64) -> u64 { - left + right -} +pub mod client; -#[cfg(test)] -mod tests { - use super::*; +use thiserror::Error; - #[test] - fn it_works() { - let result = add(2, 2); - assert_eq!(result, 4); - } +#[derive(Error, Debug)] +pub enum AuthServiceError { + #[error("invalid url provided")] + InvalidUrl(#[from] url::ParseError), + #[error("the data for key `{0}` is not available")] + Redaction(String), + #[error("invalid header (expected {expected:?}, found {found:?})")] + InvalidHeader { expected: String, found: String }, + #[error("unknown data store error")] + Unknown, } diff --git a/sellershut/Cargo.toml b/sellershut/Cargo.toml index e197acb..5ad7438 100644 --- a/sellershut/Cargo.toml +++ b/sellershut/Cargo.toml @@ -11,10 +11,13 @@ anyhow = "1.0.101" auth-service = { path = "../lib/auth-service" } axum = "0.8.8" clap = { version = "4.5.57", features = ["derive", "env"] } +secrecy = { workspace = true, features = ["serde"] } serde = { workspace = true, features = ["derive"] } +toml = "0.9.11" tracing.workspace = true tracing-appender = "0.2.4" tracing-subscriber = { version = "0.3.22", features = ["env-filter"] } +url = { workspace = true, features = ["serde"] } [dependencies.tokio] workspace = true diff --git a/sellershut/sellershut.toml b/sellershut/sellershut.toml index 047fcc6..b5e4545 100644 --- a/sellershut/sellershut.toml +++ b/sellershut/sellershut.toml @@ -2,6 +2,9 @@ port = 2210 environment = "dev" +[database] + + [oauth] redirect-url = "http://localhost:2210" diff --git a/sellershut/src/config/cli/database.rs b/sellershut/src/config/cli/database.rs new file mode 100644 index 0000000..59fac99 --- /dev/null +++ b/sellershut/src/config/cli/database.rs @@ -0,0 +1,14 @@ +use clap::Parser; +use serde::Deserialize; +use url::Url; + +#[derive(Parser, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub struct Database { + /// Database url + #[arg(long, value_name = "DATABASE_URL", env = "DATABASE_URL")] + pub database_url: Option<Url>, + /// Database pool size + #[arg(long, value_name = "DATABASE_POOL_SIZE", env = "DATABASE_POOL_SIZE")] + pub database_pool_size: Option<u32>, +} diff --git a/sellershut/src/config/cli/mod.rs b/sellershut/src/config/cli/mod.rs index 325f028..551eb4d 100644 --- a/sellershut/src/config/cli/mod.rs +++ b/sellershut/src/config/cli/mod.rs @@ -1,17 +1,48 @@ -pub mod validator; +pub mod database; +pub mod oauth; use std::path::PathBuf; use clap::Parser; +use clap::ValueEnum; +use serde::Deserialize; -#[derive(Parser)] +use crate::config::cli::{database::Database, oauth::Oauth}; + +#[derive(Parser, Deserialize, Default)] /// A federated marketplace platform #[command(version, about, long_about = None)] +#[serde(rename_all = "kebab-case")] pub struct Cli { /// Sets a custom config file #[arg(short, long, value_name = "FILE")] - config: Option<PathBuf>, + #[serde(skip)] + pub config: Option<PathBuf>, + #[command(flatten)] + pub server: Option<Server>, + #[command(flatten)] + pub oauth: Option<Oauth>, + #[command(flatten)] + pub database: Option<Database>, +} + +#[derive(Parser, Deserialize, Default)] +#[serde(rename_all = "kebab-case")] +pub struct Server { /// Sets the port that the server listens to #[arg(short, long, value_name = "PORT", env = "PORT", default_value = "2210")] #[arg(value_parser = clap::value_parser!(u16).range(1..=65535))] - port: Option<u16> + pub port: Option<u16>, + /// Runtime environment + #[arg(short, long, value_name = "ENV")] + pub environment: Option<CliEnvironment>, +} + +#[derive(Deserialize, ValueEnum, Clone, Copy, Default)] +#[serde(rename_all = "lowercase")] +pub enum CliEnvironment { + #[default] + Dev, + Development, + Prod, + Production, } diff --git a/sellershut/src/config/cli/oauth/discord.rs b/sellershut/src/config/cli/oauth/discord.rs new file mode 100644 index 0000000..afb4154 --- /dev/null +++ b/sellershut/src/config/cli/oauth/discord.rs @@ -0,0 +1,36 @@ +use clap::Parser; +use serde::Deserialize; +use url::Url; + +#[derive(Parser, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub struct DiscordConfig { + /// Discord client id + #[arg( + long, + value_name = "OAUTH_DISCORD_CLIENT_ID", + env = "OAUTH_DISCORD_CLIENT_ID" + )] + discord_client_id: Option<String>, + /// Discord client secret + #[arg( + long, + value_name = "OAUTH_DISCORD_CLIENT_SECRET", + env = "OAUTH_DISCORD_CLIENT_SECRET" + )] + discord_client_secret: Option<secrecy::SecretString>, + /// Discord auth url + #[arg( + long, + value_name = "OAUTH_DISCORD_AUTH_URL", + env = "OAUTH_DISCORD_AUTH_URL" + )] + discord_auth_url: Option<Url>, + /// Discord token url + #[arg( + long, + value_name = "OAUTH_DISCORD_TOKEN_URL", + env = "OAUTH_DISCORD_TOKEN_URL" + )] + discord_token_url: Option<Url>, +} diff --git a/sellershut/src/config/cli/oauth/mod.rs b/sellershut/src/config/cli/oauth/mod.rs new file mode 100644 index 0000000..cc59231 --- /dev/null +++ b/sellershut/src/config/cli/oauth/mod.rs @@ -0,0 +1,17 @@ +pub mod discord; + +use clap::Parser; +use serde::Deserialize; +use url::Url; + +use crate::config::cli::oauth::discord::DiscordConfig; + +#[derive(Parser, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub struct Oauth { + /// Oauth redirect url + #[arg(long, value_name = "OAUTH_REDIRECT_URL", env = "OAUTH_REDIRECT_URL")] + pub oauth_redirect_url: Option<Url>, + #[command(flatten)] + pub discord: Option<DiscordConfig>, +} diff --git a/sellershut/src/config/cli/validator.rs b/sellershut/src/config/cli/validator.rs deleted file mode 100644 index e69de29..0000000 --- a/sellershut/src/config/cli/validator.rs +++ /dev/null diff --git a/sellershut/src/config/mod.rs b/sellershut/src/config/mod.rs index be6fe1a..ee0103e 100644 --- a/sellershut/src/config/mod.rs +++ b/sellershut/src/config/mod.rs @@ -1,12 +1,105 @@ +use auth_service::client::ClientConfig; +use clap::ValueEnum; use serde::Deserialize; +use tracing::Level; +use url::Url; + +use crate::config::cli::{Cli, CliEnvironment, oauth}; pub mod cli; -#[derive(Deserialize)] +#[derive(Deserialize, ValueEnum, Clone, Copy)] #[serde(rename_all = "lowercase")] pub enum Environment { - Dev, Development, - Prod, Production, } + +impl From<CliEnvironment> for Environment { + fn from(value: CliEnvironment) -> Self { + match value { + CliEnvironment::Dev | CliEnvironment::Development => Self::Development, + CliEnvironment::Prod | CliEnvironment::Production => Self::Production, + } + } +} + +pub struct Configuration { + pub server: Server, + pub oauth: Oauth, + pub database: Database, +} + +pub struct Oauth { + pub redirect_url: Url, + pub discord: ClientConfig, +} + +pub struct Server { + pub port: u16, + pub environment: Environment, + pub log_level: Level, +} + +pub struct Database { + pub url: Url, + pub pool_size: u32, +} + +impl Configuration { + pub fn merge(cli: &Cli, file: &Cli) -> anyhow::Result<Self> { + let mut missing = Vec::new(); + let port = cli + .server + .as_ref() + .and_then(|value| value.port) + .or(file.server.as_ref().and_then(|value| value.port)); + + if port.is_none() { + missing.push("server.port"); + } + + let environment = cli + .server + .as_ref() + .and_then(|v| v.environment) + .or(file.server.as_ref().and_then(|v| v.environment)) + .unwrap_or_default() + .into(); + + let cli_oauth = cli.oauth.as_ref(); + let file_oauth = file.oauth.as_ref(); + + let oauth_redirect_url = cli_oauth + .and_then(|value| value.oauth_redirect_url.clone()) + .or(file_oauth + .and_then(|value| value.oauth_redirect_url.clone())); + + if oauth_redirect_url.is_none() { + missing.push("oauth.redirect-url"); + } + + let discord_config = cli_oauth.and_then(|v| v.discord.as_ref()).or(file_oauth.and_then(|v| v.discord.as_ref())); + + if !missing.is_empty() { + anyhow::bail!( + "Missing required configuration values:\n{}", + missing + .iter() + .map(|f| format!(" - {}", f)) + .collect::<Vec<_>>() + .join("\n") + ); + } + + Ok(Self { + server: Server { + port: port.unwrap(), + environment, + log_level: Level::INFO, + }, + oauth: todo!(), + database: todo!(), + }) + } +} diff --git a/sellershut/src/main.rs b/sellershut/src/main.rs index 4ee5c37..bd22dd5 100644 --- a/sellershut/src/main.rs +++ b/sellershut/src/main.rs @@ -1,18 +1,30 @@ mod config; +mod state; use std::time::Duration; +use anyhow::Context; use axum::{Router, routing::get}; use clap::Parser; use tokio::time::sleep; use tokio::{net::TcpListener, signal}; use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; +use crate::config::Configuration; use crate::config::cli::Cli; #[tokio::main] async fn main() -> anyhow::Result<()> { let cli = Cli::parse(); + let config = if let Some(file) = cli.config.as_ref() { + let contents = std::fs::read_to_string(file) + .with_context(|| format!("Failed to read config file: {file:?}"))?; + toml::from_str(&contents)? + } else { + Cli::default() + }; + let config = Configuration::merge(&cli, &config)?; + // Enable tracing. tracing_subscriber::registry() .with( diff --git a/sellershut/src/state/mod.rs b/sellershut/src/state/mod.rs new file mode 100644 index 0000000..cf659c5 --- /dev/null +++ b/sellershut/src/state/mod.rs @@ -0,0 +1,5 @@ +use auth_service::client::OauthClient; + +pub struct AppState { + discord_client: OauthClient, +} |
