From 2c35b9f65db1a3247d5721e011ed8bf7cce69332 Mon Sep 17 00:00:00 2001 From: JerryImMouse Date: Mon, 31 Aug 2026 14:42:23 +0500 Subject: [PATCH 1/2] feat: callback route now can redirect to specified uri --- Cargo.lock | 1 + Cargo.toml | 2 +- README.md | 8 ++++++-- config.toml | 3 +++ src/config/mod.rs | 26 ++++++++++++++++++++++++-- src/config/runtime.rs | 20 +++++++++++++++++++- src/web/routes/auth.rs | 13 +++++++++---- 7 files changed, 63 insertions(+), 10 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 11c4550..2ba956a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2496,6 +2496,7 @@ dependencies = [ "idna", "percent-encoding", "serde", + "serde_derive", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 60774d4..eddd849 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,7 +21,7 @@ uuid = { version = "1.25", features = ["v4", "serde"] } chrono = { version = "0.4", features = ["serde"]} reqwest = { version = "0.13", features = ["json", "form"] } jsonwebtoken = { version = "11", features = ["aws_lc_rs"] } -url = { version = "2.5" } +url = { version = "2.5", features = ["serde"] } utoipa = { version = "5.5", features = ["yaml", "chrono"], optional = true } diff --git a/README.md b/README.md index 1c0881a..21590fa 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,7 @@ Open the resulting file in any OpenAPI viewer (Swagger UI, Redoc, etc.) to brows ## Setup -Mantle is configured via a required `config.toml` in the project root — every field must be present. Any field can be overridden via environment variables (e.g. for secrets you don't want committed). +Mantle is configured via a required `config.toml` in the project root — every field(except `redirect_uri`) must be present. Any field can be overridden via environment variables (e.g. for secrets you don't want committed). ```toml [database] @@ -68,7 +68,10 @@ api_secret = "..." client_id = "..." client_secret = "..." redirect_uri = "https://your-domain/api/auth/callback" -state_secret = "..." +state_secret = "..." + +[app] +redirect_uri = "https://example.com" # the only optional field in the config ``` @@ -82,6 +85,7 @@ state_secret = "..." | `APP_DISCORD_CLIENT_SECRET` | `discord.client_secret` | | `APP_DISCORD_REDIRECT_URI` | `discord.redirect_uri` | | `APP_DISCORD_STATE_SECRET` | `discord.state_secret` | +| `APP_CALLBACK_REDIRECT_URI` | `app.redirect_uri` | Once `config.toml` is in place, it's a regular Rust project: diff --git a/config.toml b/config.toml index 7427bff..a264dca 100644 --- a/config.toml +++ b/config.toml @@ -11,3 +11,6 @@ client_id = "change-me" client_secret = "change-me" redirect_uri = "http://localhost:5050/api/auth/callback" state_secret = "change-me" + +[app] +redirect_uri = "https://example.com" diff --git a/src/config/mod.rs b/src/config/mod.rs index 71f34ff..0b67c9e 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -5,12 +5,16 @@ pub use error::ConfigError; mod runtime; pub use runtime::*; +use url::Url; #[derive(Debug, Serialize, Deserialize)] pub struct Config { server: ServerConfig, database: DatabaseConfig, discord: DiscordConfig, + + #[serde(default)] + app: AppConfig, } #[derive(Debug, Serialize, Deserialize)] @@ -33,6 +37,11 @@ pub struct DiscordConfig { state_secret: String, } +#[derive(Debug, Default, Serialize, Deserialize)] +pub struct AppConfig { + redirect_uri: Option, +} + impl Config { #[tracing::instrument] pub fn load_from_file + std::fmt::Debug>( @@ -51,6 +60,7 @@ impl Config { self.server.apply_env(); self.database.apply_env(); self.discord.apply_env(); + self.app.apply_env(); self } } @@ -78,17 +88,29 @@ impl DiscordConfig { } } +impl AppConfig { + pub fn apply_env(&mut self) { + // some hack, probably can use unsafe or smth to do it, but I won't :P + let mut url = Url::parse("http://example.com").unwrap(); + if override_parse("APP_CALLBACK_REDIRECT_URI", &mut url) { + self.redirect_uri = Some(url); + } + } +} + fn override_string(key: &str, target: &mut String) { if let Ok(v) = dotenvy::var(key) { *target = v; } } -fn override_parse(key: &str, target: &mut T) +fn override_parse(key: &str, target: &mut T) -> bool where T::Err: std::fmt::Debug, { if let Ok(v) = dotenvy::var(key) { - *target = v.parse().expect("invalid environment value") + *target = v.parse().expect("invalid environment value"); + return true; } + return false; } diff --git a/src/config/runtime.rs b/src/config/runtime.rs index fbde1b1..5b0f310 100644 --- a/src/config/runtime.rs +++ b/src/config/runtime.rs @@ -1,12 +1,15 @@ use std::net::Ipv4Addr; -use crate::config::{Config, ConfigError, DatabaseConfig, DiscordConfig, ServerConfig}; +use url::Url; + +use crate::config::{AppConfig, Config, ConfigError, DatabaseConfig, DiscordConfig, ServerConfig}; #[derive(Debug)] pub struct RuntimeConfig { pub server: RuntimeServerConfig, pub database: RuntimeDatabaseConfig, pub discord: RuntimeDiscordConfig, + pub app: RuntimeAppConfig, } #[derive(Debug)] @@ -24,6 +27,11 @@ pub struct RuntimeDiscordConfig { pub state_secret: String, } +#[derive(Debug)] +pub struct RuntimeAppConfig { + pub redirect_uri: Option, +} + #[derive(Debug)] pub struct RuntimeDatabaseConfig { pub url: String, @@ -40,10 +48,12 @@ impl Config { let server = self.server.validate()?; let database = self.database.validate()?; let discord = self.discord.validate()?; + let app = self.app.validate()?; Ok(RuntimeConfig { server, database, discord, + app }) } } @@ -65,6 +75,14 @@ impl DiscordConfig { } } +impl AppConfig { + fn validate(self) -> Result { + Ok(RuntimeAppConfig { + redirect_uri: self.redirect_uri, + }) + } +} + impl ServerConfig { fn validate(self) -> Result { let addr: std::net::Ipv4Addr = self diff --git a/src/web/routes/auth.rs b/src/web/routes/auth.rs index ef6123e..9f99648 100644 --- a/src/web/routes/auth.rs +++ b/src/web/routes/auth.rs @@ -3,7 +3,7 @@ use axum::{ extract::{Query, State}, http::StatusCode, middleware, - response::IntoResponse, + response::{IntoResponse, Redirect, Response}, routing::{get, post}, }; @@ -83,7 +83,8 @@ async fn check( tag = "auth", security(()), responses( - (status = 200), + (status = 200, description = "if redirect_uri is not set in the config, this reponse will be returned"), + (status = 308, description = "if the redirect_uri IS set - then the user will be redirected to specfied URI"), (status = "default", body = openapi::ErrorResponse), ) ) @@ -92,12 +93,16 @@ async fn check( async fn callback( state: State, req: Query, -) -> RouteResult { +) -> RouteResult { state .discord_oauth .process_callback(&req.code, &req.state) .await?; - Ok(StatusCode::OK) + if let Some(redirect_uri) = state.config.app.redirect_uri.as_ref() { + Ok(Redirect::permanent(redirect_uri.as_str()).into_response()) + } else { + Ok(StatusCode::OK.into_response()) + } } #[cfg_attr( From 30f8be07b3829fb1d1e0f6239112ced706d882f7 Mon Sep 17 00:00:00 2001 From: JerryImMouse Date: Mon, 31 Aug 2026 14:51:41 +0500 Subject: [PATCH 2/2] docs: fix utoipa docs on link and callback routes --- src/config/mod.rs | 5 +++-- src/config/runtime.rs | 2 +- src/web/dto/auth.rs | 6 ++++-- src/web/routes/auth.rs | 17 +++++------------ 4 files changed, 13 insertions(+), 17 deletions(-) diff --git a/src/config/mod.rs b/src/config/mod.rs index 0b67c9e..b24b5d2 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -110,7 +110,8 @@ where { if let Ok(v) = dotenvy::var(key) { *target = v.parse().expect("invalid environment value"); - return true; + true + } else { + false } - return false; } diff --git a/src/config/runtime.rs b/src/config/runtime.rs index 5b0f310..15338a3 100644 --- a/src/config/runtime.rs +++ b/src/config/runtime.rs @@ -53,7 +53,7 @@ impl Config { server, database, discord, - app + app, }) } } diff --git a/src/web/dto/auth.rs b/src/web/dto/auth.rs index 9e7a0fb..267943d 100644 --- a/src/web/dto/auth.rs +++ b/src/web/dto/auth.rs @@ -12,14 +12,16 @@ pub struct CheckResponseBody { pub status: String, } -#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] +#[cfg_attr(feature = "openapi", derive(utoipa::IntoParams))] +#[cfg_attr(feature = "openapi", into_params(parameter_in = Query))] #[derive(Debug, Serialize, Deserialize)] pub struct CallbackRequestQuery { pub code: String, pub state: String, } -#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] +#[cfg_attr(feature = "openapi", derive(utoipa::IntoParams))] +#[cfg_attr(feature = "openapi", into_params(parameter_in = Query))] #[derive(Debug, Deserialize)] pub struct GenerateLinkRequestQuery { pub user_id: String, diff --git a/src/web/routes/auth.rs b/src/web/routes/auth.rs index 9f99648..43fbc39 100644 --- a/src/web/routes/auth.rs +++ b/src/web/routes/auth.rs @@ -76,9 +76,8 @@ async fn check( get, path = "/api/auth/callback", description = "Discord callback should point here", - request_body( - content = CallbackRequestQuery, - description = "This should be supplied by discord itself" + params( + CallbackRequestQuery, ), tag = "auth", security(()), @@ -111,9 +110,8 @@ async fn callback( get, path = "/api/auth/link", description = "Generate discord OAuth2 link", - request_body( - content = GenerateLinkRequestQuery, - description = "Provide an External UserID as `user_id`" + params( + GenerateLinkRequestQuery, ), tag = "auth", responses( @@ -140,12 +138,7 @@ pub mod openapi { #[derive(utoipa::OpenApi)] #[openapi( paths(check, generate_link, callback,), - components(schemas( - CheckRequestBody, - CheckResponseBody, - GenerateLinkRequestQuery, - GenerateLinkResponseBody, - )) + components(schemas(CheckRequestBody, CheckResponseBody, GenerateLinkResponseBody,)) )] pub struct ApiDoc; }