Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }

Expand Down
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -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

```

Expand All @@ -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:

Expand Down
3 changes: 3 additions & 0 deletions config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
27 changes: 25 additions & 2 deletions src/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand All @@ -33,6 +37,11 @@ pub struct DiscordConfig {
state_secret: String,
}

#[derive(Debug, Default, Serialize, Deserialize)]
pub struct AppConfig {
redirect_uri: Option<Url>,
}

impl Config {
#[tracing::instrument]
pub fn load_from_file<P: AsRef<std::path::Path> + std::fmt::Debug>(
Expand All @@ -51,6 +60,7 @@ impl Config {
self.server.apply_env();
self.database.apply_env();
self.discord.apply_env();
self.app.apply_env();
self
}
}
Expand Down Expand Up @@ -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<T: std::str::FromStr>(key: &str, target: &mut T)
fn override_parse<T: std::str::FromStr>(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
}
}
20 changes: 19 additions & 1 deletion src/config/runtime.rs
Original file line number Diff line number Diff line change
@@ -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)]
Expand All @@ -24,6 +27,11 @@ pub struct RuntimeDiscordConfig {
pub state_secret: String,
}

#[derive(Debug)]
pub struct RuntimeAppConfig {
pub redirect_uri: Option<Url>,
}

#[derive(Debug)]
pub struct RuntimeDatabaseConfig {
pub url: String,
Expand All @@ -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,
})
}
}
Expand All @@ -65,6 +75,14 @@ impl DiscordConfig {
}
}

impl AppConfig {
fn validate(self) -> Result<RuntimeAppConfig, ConfigError> {
Ok(RuntimeAppConfig {
redirect_uri: self.redirect_uri,
})
}
}

impl ServerConfig {
fn validate(self) -> Result<RuntimeServerConfig, ConfigError> {
let addr: std::net::Ipv4Addr = self
Expand Down
6 changes: 4 additions & 2 deletions src/web/dto/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
30 changes: 14 additions & 16 deletions src/web/routes/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use axum::{
extract::{Query, State},
http::StatusCode,
middleware,
response::IntoResponse,
response::{IntoResponse, Redirect, Response},
routing::{get, post},
};

Expand Down Expand Up @@ -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),
)
)
Expand All @@ -92,12 +92,16 @@ async fn check(
async fn callback(
state: State<AppState>,
req: Query<CallbackRequestQuery>,
) -> RouteResult<impl IntoResponse> {
) -> RouteResult<Response> {
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(
Expand All @@ -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(
Expand All @@ -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;
}
Loading