diff options
-rw-r--r-- | Cargo.lock | 91 | ||||
-rw-r--r-- | Cargo.toml | 3 | ||||
-rw-r--r-- | contrib/docker-compose/init-db/init.sql | 1 | ||||
-rw-r--r-- | crates/auth/Cargo.toml | 11 | ||||
-rw-r--r-- | crates/auth/auth.toml | 24 | ||||
-rw-r--r-- | crates/auth/migrations/20250723100947_account.sql | 1 | ||||
-rw-r--r-- | crates/auth/src/client.rs | 6 | ||||
-rw-r--r-- | crates/auth/src/client/discord.rs | 30 | ||||
-rw-r--r-- | crates/auth/src/cnfg.rs | 23 | ||||
-rw-r--r-- | crates/auth/src/error.rs | 26 | ||||
-rw-r--r-- | crates/auth/src/main.rs | 69 | ||||
-rw-r--r-- | crates/auth/src/server.rs | 28 | ||||
-rw-r--r-- | crates/auth/src/server/routes.rs | 47 | ||||
-rw-r--r-- | crates/auth/src/server/routes/authorised.rs | 23 | ||||
-rw-r--r-- | crates/auth/src/server/routes/discord.rs | 10 | ||||
-rw-r--r-- | crates/auth/src/server/routes/discord/discord_auth.rs | 20 | ||||
-rw-r--r-- | crates/auth/src/state.rs | 45 | ||||
-rw-r--r-- | crates/sellershut/Cargo.toml | 4 |
18 files changed, 453 insertions, 9 deletions
@@ -185,16 +185,19 @@ dependencies = [ ] [[package]] -name = "auth" +name = "auth-service" version = "0.1.0" dependencies = [ "anyhow", "axum", + "axum-extra", "base64", "clap", "config", "futures-util", "nanoid", + "oauth2", + "reqwest", "serde", "serde_json", "sqlx", @@ -269,6 +272,29 @@ dependencies = [ ] [[package]] +name = "axum-extra" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45bf463831f5131b7d3c756525b305d40f1185b688565648a92e1392ca35713d" +dependencies = [ + "axum", + "axum-core", + "bytes", + "futures-util", + "headers", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "rustversion", + "serde", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] name = "axum-macros" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -396,7 +422,10 @@ checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" dependencies = [ "android-tzdata", "iana-time-zone", + "js-sys", "num-traits", + "serde", + "wasm-bindgen", "windows-link", ] @@ -461,6 +490,7 @@ version = "0.15.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b1eb4fb07bc7f012422df02766c7bd5971effb894f573865642f06fa3265440" dependencies = [ + "convert_case", "pathdiff", "serde", "toml", @@ -474,6 +504,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" [[package]] +name = "convert_case" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" +dependencies = [ + "unicode-segmentation", +] + +[[package]] name = "core-foundation-sys" version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -964,6 +1003,30 @@ dependencies = [ ] [[package]] +name = "headers" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3314d5adb5d94bcdf56771f2e50dbbc80bb4bdf88967526706205ac9eff24eb" +dependencies = [ + "base64", + "bytes", + "headers-core", + "http", + "httpdate", + "mime", + "sha1", +] + +[[package]] +name = "headers-core" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54b4a22553d4242c49fddb9ba998a99962b5cc6f22cb5a3482bec22522403ce4" +dependencies = [ + "http", +] + +[[package]] name = "heck" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1564,6 +1627,26 @@ dependencies = [ ] [[package]] +name = "oauth2" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51e219e79014df21a225b1860a479e2dcd7cbd9130f4defd4bd0e191ea31d67d" +dependencies = [ + "base64", + "chrono", + "getrandom 0.2.16", + "http", + "rand 0.8.5", + "reqwest", + "serde", + "serde_json", + "serde_path_to_error", + "sha2", + "thiserror 1.0.69", + "url", +] + +[[package]] name = "object" version = "0.36.7" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2967,6 +3050,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0" [[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + +[[package]] name = "untrusted" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -12,14 +12,17 @@ description = "A federated marketplace platform" anyhow = "1.0.98" async-trait = "0.1.88" axum = "0.8.4" +base64 = "0.22.1" clap = "4.5.41" config = { version = "0.15.13", default-features = false } futures-util = { version = "0.3.31", default-features = false } nanoid = "0.4.0" +reqwest = { version = "0.12.22", default-features = false } serde = "1.0.219" serde_json = "1.0.140" sqlx = "0.8.6" stack-up = { git = "https://github.com/rtkay123/stack-up.git" } +time = { version = "0.3.41", default-features = false } tokio = "1.46.1" tower = "0.5.2" tower-http = "0.6.6" diff --git a/contrib/docker-compose/init-db/init.sql b/contrib/docker-compose/init-db/init.sql index 6ccb280..54ec5d6 100644 --- a/contrib/docker-compose/init-db/init.sql +++ b/contrib/docker-compose/init-db/init.sql @@ -1 +1,2 @@ create database sellershut; +create database auth; diff --git a/crates/auth/Cargo.toml b/crates/auth/Cargo.toml index e610c79..b5e53d9 100644 --- a/crates/auth/Cargo.toml +++ b/crates/auth/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "auth" +name = "auth-service" version = "0.1.0" edition = "2024" license.workspace = true @@ -10,15 +10,18 @@ description.workspace = true [dependencies] anyhow.workspace = true axum = { workspace = true, features = ["macros"] } -base64 = "0.22.1" +axum-extra = { version = "0.10.1", features = ["typed-header"] } +base64.workspace = true clap = { workspace = true, features = ["derive"] } -config = { workspace = true, features = ["toml"] } +config = { workspace = true, features = ["convert-case", "toml"] } futures-util.workspace = true nanoid.workspace = true +oauth2 = "5.0.0" +reqwest = { workspace = true, features = ["json", "rustls-tls"] } serde = { workspace = true, features = ["derive"] } serde_json.workspace = true sqlx = { workspace = true, features = ["macros", "migrate", "runtime-tokio", "time", "tls-rustls", "uuid"] } -time = { version = "0.3.41", default-features = false, features = ["parsing", "serde"] } +time = { workspace = true, features = ["parsing", "serde"] } tokio = { workspace = true, features = ["macros", "rt-multi-thread", "signal"] } tower = { workspace = true, features = ["util"] } tower-http = { workspace = true, features = ["map-request-body", "trace", "util"] } diff --git a/crates/auth/auth.toml b/crates/auth/auth.toml new file mode 100644 index 0000000..cfb6501 --- /dev/null +++ b/crates/auth/auth.toml @@ -0,0 +1,24 @@ +[application] +env = "development" +port = 1304 + +[misc.oauth.discord] +# query param for provider +redirect-url = "http://127.0.0.1:1304/auth/authorised?provider=discord" +#client-id = "" +#client-secret = "" +#auth-url = "" + + +[monitoring] +log-level = "auth_service=trace,info" + +[database] +pool_size = 100 +port = 5432 +name = "auth" +host = "localhost" +password = "password" +user = "postgres" + +# vim:ft=toml diff --git a/crates/auth/migrations/20250723100947_account.sql b/crates/auth/migrations/20250723100947_account.sql new file mode 100644 index 0000000..8ddc1d3 --- /dev/null +++ b/crates/auth/migrations/20250723100947_account.sql @@ -0,0 +1 @@ +-- Add migration script here diff --git a/crates/auth/src/client.rs b/crates/auth/src/client.rs new file mode 100644 index 0000000..5aa4de0 --- /dev/null +++ b/crates/auth/src/client.rs @@ -0,0 +1,6 @@ +use oauth2::{EndpointNotSet, EndpointSet, basic::BasicClient}; + +pub mod discord; + +pub type OauthClient = + BasicClient<EndpointSet, EndpointNotSet, EndpointNotSet, EndpointNotSet, EndpointSet>; diff --git a/crates/auth/src/client/discord.rs b/crates/auth/src/client/discord.rs new file mode 100644 index 0000000..9217684 --- /dev/null +++ b/crates/auth/src/client/discord.rs @@ -0,0 +1,30 @@ +use crate::{client::OauthClient, cnfg::OauthCredentials, error::AppError}; +use anyhow::Context; +use oauth2::{AuthUrl, ClientId, ClientSecret, RedirectUrl, TokenUrl, basic::BasicClient}; + +pub fn discord_client(config: &OauthCredentials) -> Result<OauthClient, AppError> { + let auth_url = config.auth_url.clone().unwrap_or_else(|| { + "https://discord.com/api/oauth2/authorize?response_type=code".to_string() + }); + + let token_url = config + .token_url + .clone() + .unwrap_or_else(|| "https://discord.com/api/oauth2/token".to_string()); + + let c = BasicClient::new(ClientId::new(config.client_id.to_owned())) + .set_client_secret(ClientSecret::new(config.client_secret.to_owned())) + .set_auth_uri( + AuthUrl::new(auth_url).context("failed to create new auth server url [discord]")?, + ) + .set_redirect_uri( + RedirectUrl::new(config.redirect_url.to_owned()) + .context("failed to create new redirect URL [discord]")?, + ) + .set_token_uri( + TokenUrl::new(token_url) + .context("failed to create new token endpoint URL [discord]")?, + ); + + Ok(c) +} diff --git a/crates/auth/src/cnfg.rs b/crates/auth/src/cnfg.rs new file mode 100644 index 0000000..6afe2f8 --- /dev/null +++ b/crates/auth/src/cnfg.rs @@ -0,0 +1,23 @@ +use serde::Deserialize; + +#[derive(Deserialize, Clone)] +#[serde(rename_all = "kebab-case")] +pub struct LocalConfig { + pub oauth: OauthConfig, +} + +#[derive(Deserialize, Clone)] +#[serde(rename_all = "kebab-case")] +pub struct OauthConfig { + pub discord: OauthCredentials, +} + +#[derive(Deserialize, Clone)] +#[serde(rename_all = "kebab-case")] +pub struct OauthCredentials { + pub client_id: String, + pub client_secret: String, + pub redirect_url: String, + pub auth_url: Option<String>, + pub token_url: Option<String>, +} diff --git a/crates/auth/src/error.rs b/crates/auth/src/error.rs new file mode 100644 index 0000000..730f99a --- /dev/null +++ b/crates/auth/src/error.rs @@ -0,0 +1,26 @@ +use axum::{ + http::StatusCode, + response::{IntoResponse, Response}, +}; + +#[derive(Debug)] +pub struct AppError(anyhow::Error); + +impl IntoResponse for AppError { + fn into_response(self) -> Response { + ( + StatusCode::INTERNAL_SERVER_ERROR, + format!("Something went wrong: {}", self.0), + ) + .into_response() + } +} + +impl<E> From<E> for AppError +where + E: Into<anyhow::Error>, +{ + fn from(err: E) -> Self { + Self(err.into()) + } +} diff --git a/crates/auth/src/main.rs b/crates/auth/src/main.rs index e7a11a9..4a71d69 100644 --- a/crates/auth/src/main.rs +++ b/crates/auth/src/main.rs @@ -1,3 +1,68 @@ -fn main() { - println!("Hello, world!"); +mod client; +mod cnfg; +mod error; +mod server; +mod state; + +use std::net::{Ipv6Addr, SocketAddr}; + +use clap::Parser; +use stack_up::{Configuration, Services, tracing::Tracing}; +use tracing::{info, trace}; + +use crate::{error::AppError, state::AppState}; + +/// auth-service +#[derive(Parser, Debug)] +#[command(version, about, long_about = None)] +struct Args { + /// Path to config file + #[arg(short, long)] + config_file: Option<std::path::PathBuf>, +} + +#[tokio::main] +async fn main() -> Result<(), AppError> { + let args = Args::parse(); + let config = include_str!("../auth.toml"); + + let mut config = config::Config::builder() + .add_source(config::File::from_str(config, config::FileFormat::Toml)) + .add_source( + config::Environment::with_prefix("APP") + .separator("__") + .convert_case(config::Case::Kebab), + ); + + if let Some(cf) = args.config_file.as_ref().and_then(|v| v.to_str()) { + config = config.add_source(config::File::new(cf, config::FileFormat::Toml)); + }; + + let mut config: Configuration = config.build()?.try_deserialize()?; + dbg!(&config); + config.application.name = env!("CARGO_CRATE_NAME").into(); + config.application.version = env!("CARGO_PKG_VERSION").into(); + + let _tracing = Tracing::builder().build(&config.monitoring); + + let services = Services::builder() + .postgres(&config.database) + .await + .inspect_err(|e| tracing::error!("database: {e}"))? + .build(); + + trace!("running migrations"); + sqlx::migrate!("./migrations") + .run(&services.postgres) + .await?; + + let state = AppState::create(services, &config).await?; + + let addr = SocketAddr::from((Ipv6Addr::UNSPECIFIED, config.application.port)); + + let listener = tokio::net::TcpListener::bind(addr).await?; + info!(port = addr.port(), "serving api"); + + axum::serve(listener, server::router(state)).await?; + Ok(()) } diff --git a/crates/auth/src/server.rs b/crates/auth/src/server.rs new file mode 100644 index 0000000..3cfac60 --- /dev/null +++ b/crates/auth/src/server.rs @@ -0,0 +1,28 @@ +use axum::{Router, routing::get}; +use tower_http::trace::TraceLayer; + +use crate::{server::routes::health_check, state::AppHandle}; + +pub mod routes; + +pub fn router(state: AppHandle) -> Router { + Router::new() + .merge(routes::discord::discord_router(state.clone())) + .route("/", get(health_check)) + .route("/auth/authorised", get(health_check)) + .layer(TraceLayer::new_for_http()) +} + +#[cfg(test)] +pub(crate) fn test_config() -> stack_up::Configuration { + use stack_up::Configuration; + + let config_path = "auth.toml"; + + let config = config::Config::builder() + .add_source(config::File::new(config_path, config::FileFormat::Toml)) + .build() + .unwrap(); + + config.try_deserialize::<Configuration>().unwrap() +} diff --git a/crates/auth/src/server/routes.rs b/crates/auth/src/server/routes.rs new file mode 100644 index 0000000..7a25e70 --- /dev/null +++ b/crates/auth/src/server/routes.rs @@ -0,0 +1,47 @@ +pub mod authorised; +pub mod discord; +use axum::response::IntoResponse; +use serde::Deserialize; + +#[derive(Debug, Clone, Copy, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum Provider { + Discord, +} + +pub async fn health_check() -> impl IntoResponse { + let name = env!("CARGO_PKG_NAME"); + let ver = env!("CARGO_PKG_VERSION"); + + format!("{name} v{ver} is live") +} + +#[cfg(test)] +mod tests { + use axum::{ + body::Body, + http::{Request, StatusCode}, + }; + use sqlx::PgPool; + use stack_up::Services; + use tower::ServiceExt; + + use crate::{ + server::{self, test_config}, + state::AppState, + }; + + #[sqlx::test] + async fn health_check(pool: PgPool) { + let services = Services { postgres: pool }; + let state = AppState::create(services, &test_config()).await.unwrap(); + let app = server::router(state); + + let response = app + .oneshot(Request::builder().uri("/").body(Body::empty()).unwrap()) + .await + .unwrap(); + + assert_eq!(response.status(), StatusCode::OK); + } +} diff --git a/crates/auth/src/server/routes/authorised.rs b/crates/auth/src/server/routes/authorised.rs new file mode 100644 index 0000000..ddf048d --- /dev/null +++ b/crates/auth/src/server/routes/authorised.rs @@ -0,0 +1,23 @@ +use axum::{ + extract::{Query, State}, + response::IntoResponse, +}; +use axum_extra::{TypedHeader, headers}; +use serde::Deserialize; + +use crate::{error::AppError, server::routes::Provider, state::AppHandle}; + +#[derive(Debug, Deserialize)] +pub struct AuthRequest { + provider: Provider, + code: String, + state: String, +} + +async fn login_authorized( + Query(query): Query<AuthRequest>, + State(state): State<AppHandle>, + TypedHeader(cookies): TypedHeader<headers::Cookie>, +) -> Result<impl IntoResponse, AppError> { + Ok("") +} diff --git a/crates/auth/src/server/routes/discord.rs b/crates/auth/src/server/routes/discord.rs new file mode 100644 index 0000000..e1a834f --- /dev/null +++ b/crates/auth/src/server/routes/discord.rs @@ -0,0 +1,10 @@ +mod discord_auth; +use axum::{Router, routing::get}; + +use crate::state::AppHandle; + +pub fn discord_router(state: AppHandle) -> Router { + Router::new() + .route("/auth/discord", get(discord_auth::discord_auth)) + .with_state(state) +} diff --git a/crates/auth/src/server/routes/discord/discord_auth.rs b/crates/auth/src/server/routes/discord/discord_auth.rs new file mode 100644 index 0000000..b07fa7a --- /dev/null +++ b/crates/auth/src/server/routes/discord/discord_auth.rs @@ -0,0 +1,20 @@ +use axum::{ + extract::State, + http::HeaderMap, + response::{IntoResponse, Redirect}, +}; +use oauth2::{CsrfToken, Scope}; + +use crate::{error::AppError, state::AppHandle}; + +pub async fn discord_auth(State(state): State<AppHandle>) -> Result<impl IntoResponse, AppError> { + let (auth_url, csrf_token) = state + .discord_client + .authorize_url(CsrfToken::new_random) + .add_scope(Scope::new("identify".to_string())) + .url(); + + let mut headers = HeaderMap::new(); + + Ok((headers, Redirect::to(auth_url.as_ref()))) +} diff --git a/crates/auth/src/state.rs b/crates/auth/src/state.rs new file mode 100644 index 0000000..5a483c9 --- /dev/null +++ b/crates/auth/src/state.rs @@ -0,0 +1,45 @@ +use std::{ops::Deref, sync::Arc}; + +use stack_up::{Configuration, Services}; + +use crate::{ + client::{OauthClient, discord::discord_client}, + cnfg::LocalConfig, + error::AppError, +}; + +#[derive(Clone)] +pub struct AppHandle(Arc<AppState>); + +impl Deref for AppHandle { + type Target = Arc<AppState>; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +pub struct AppState { + pub services: Services, + pub local_config: LocalConfig, + pub discord_client: OauthClient, + pub http_client: reqwest::Client, +} + +impl AppState { + pub async fn create( + services: Services, + configuration: &Configuration, + ) -> Result<AppHandle, AppError> { + let local_config: LocalConfig = serde_json::from_value(configuration.misc.clone())?; + + let discord_client = discord_client(&local_config.oauth.discord)?; + + Ok(AppHandle(Arc::new(Self { + services, + local_config, + discord_client, + http_client: reqwest::Client::new(), + }))) + } +} diff --git a/crates/sellershut/Cargo.toml b/crates/sellershut/Cargo.toml index a730dd3..34e7b8b 100644 --- a/crates/sellershut/Cargo.toml +++ b/crates/sellershut/Cargo.toml @@ -12,7 +12,7 @@ activitypub_federation = { version = "0.7.0-beta.5", default-features = false, f anyhow.workspace = true async-trait.workspace = true axum = { workspace = true, features = ["macros"] } -base64 = "0.22.1" +base64.workspace = true clap = { workspace = true, features = ["derive"] } config = { workspace = true, features = ["toml"] } enum_delegate = "0.2.0" @@ -23,7 +23,7 @@ serde = { workspace = true, features = ["derive"] } serde_json.workspace = true sha2 = "0.10.9" sqlx = { workspace = true, features = ["macros", "migrate", "runtime-tokio", "time", "tls-rustls", "uuid"] } -time = { version = "0.3.41", default-features = false, features = ["parsing", "serde"] } +time = { workspace = true, features = ["parsing", "serde"] } tokio = { workspace = true, features = ["macros", "rt-multi-thread", "signal"] } tower = { workspace = true, features = ["util"] } tower-http = { workspace = true, features = ["map-request-body", "trace", "util"] } |