summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Cargo.lock91
-rw-r--r--Cargo.toml3
-rw-r--r--contrib/docker-compose/init-db/init.sql1
-rw-r--r--crates/auth/Cargo.toml11
-rw-r--r--crates/auth/auth.toml24
-rw-r--r--crates/auth/migrations/20250723100947_account.sql1
-rw-r--r--crates/auth/src/client.rs6
-rw-r--r--crates/auth/src/client/discord.rs30
-rw-r--r--crates/auth/src/cnfg.rs23
-rw-r--r--crates/auth/src/error.rs26
-rw-r--r--crates/auth/src/main.rs69
-rw-r--r--crates/auth/src/server.rs28
-rw-r--r--crates/auth/src/server/routes.rs47
-rw-r--r--crates/auth/src/server/routes/authorised.rs23
-rw-r--r--crates/auth/src/server/routes/discord.rs10
-rw-r--r--crates/auth/src/server/routes/discord/discord_auth.rs20
-rw-r--r--crates/auth/src/state.rs45
-rw-r--r--crates/sellershut/Cargo.toml4
18 files changed, 453 insertions, 9 deletions
diff --git a/Cargo.lock b/Cargo.lock
index f637fc1..78bb019 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -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"
diff --git a/Cargo.toml b/Cargo.toml
index 5cfc844..1283ad4 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -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"] }