aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Cargo.lock92
-rw-r--r--Cargo.toml2
-rw-r--r--lib/auth-service/Cargo.toml3
-rw-r--r--lib/auth-service/src/client/mod.rs49
-rw-r--r--lib/auth-service/src/lib.rs23
-rw-r--r--sellershut/Cargo.toml3
-rw-r--r--sellershut/sellershut.toml3
-rw-r--r--sellershut/src/config/cli/database.rs14
-rw-r--r--sellershut/src/config/cli/mod.rs39
-rw-r--r--sellershut/src/config/cli/oauth/discord.rs36
-rw-r--r--sellershut/src/config/cli/oauth/mod.rs17
-rw-r--r--sellershut/src/config/cli/validator.rs0
-rw-r--r--sellershut/src/config/mod.rs99
-rw-r--r--sellershut/src/main.rs12
-rw-r--r--sellershut/src/state/mod.rs5
15 files changed, 379 insertions, 18 deletions
diff --git a/Cargo.lock b/Cargo.lock
index e2755f2..6d62eee 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -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"
diff --git a/Cargo.toml b/Cargo.toml
index 2a943f8..315004b 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -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,
+}