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..b24b5d2 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,30 @@ 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"); + true + } else { + false } } diff --git a/src/config/runtime.rs b/src/config/runtime.rs index fbde1b1..15338a3 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/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 ef6123e..43fbc39 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}, }; @@ -76,14 +76,14 @@ 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(()), 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 +92,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( @@ -106,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( @@ -135,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; }