use crate::auth_service::AuthService;
#[cfg(feature = "axum")]
use axum::{
Json, Router,
extract::{Path, Query, State},
response::{IntoResponse, Response},
};
use serde::Deserialize;
use std::sync::Arc;
fn url_encode(s: &str) -> String {
s.bytes()
.map(|b| match b {
b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => {
(b as char).to_string()
}
_ => format!("%{b:02X}"),
})
.collect()
}
#[cfg(feature = "axum")]
pub async fn start_server(
auth_service: Arc<AuthService>,
address: impl Into<Option<std::net::SocketAddr>>,
) {
use axum::serve;
use tokio::net::TcpListener;
let app = get_authen_axum_router(auth_service.clone());
let addr = address
.into()
.unwrap_or_else(|| std::net::SocketAddr::from(([0, 0, 0, 0], 3000)));
log::info!("Axum server running at http://{addr}");
let listener = TcpListener::bind(addr).await.unwrap();
serve(listener, app).await.unwrap();
}
#[cfg(feature = "axum")]
pub fn get_authen_axum_router(auth_service: Arc<AuthService>) -> Router {
use axum::routing::{get, post};
Router::new()
.route("/signup", post(signup_handler))
.route("/login", post(login_handler))
.route("/health", get(health_handler).post(health_handler))
.route("/token/refresh", post(refresh_token_handler))
.route("/token/validate", post(validate_token_handler))
.route("/oauth/{provider}/auth", get(oauth_auth_handler))
.route("/oauth/{provider}/callback", get(oauth_callback_handler))
.route("/oauth/signup", post(oauth_signup_handler))
.route("/oauth/login", post(oauth_login_handler))
.with_state(auth_service)
}
#[cfg(feature = "axum")]
async fn signup_handler(
State(_auth): State<Arc<AuthService>>,
Json(_body): Json<serde_json::Value>,
) -> Response {
log::info!("Received /signup request: {_body}");
#[derive(Deserialize)]
struct SignupRequest {
username: String,
password: String,
}
let req: Result<SignupRequest, _> = serde_json::from_value(_body);
match req {
Ok(signup) => {
log::info!(
"Attempting signup for user: {username}",
username = signup.username
);
match _auth
.signup(crate::auth_service::SignupMethod::Credentials {
identifier: signup.username,
password: signup.password,
})
.await
{
Ok((user, _tokens)) => {
log::info!("Signup successful for user_id: {id}", id = user.id);
serde_json::json!({
"id": user.id,
"identifier": user.credentials.as_ref().map(|c| &c.identifier).unwrap_or(&"".to_string())
})
.to_string().into_response()
}
Err(e) => {
log::error!("Signup failed: {e}");
(
axum::http::StatusCode::BAD_REQUEST,
serde_json::json!({
"error": e.to_string()
})
.to_string(),
)
.into_response()
}
}
}
Err(e) => {
log::error!("Invalid signup request body: {e}");
(
axum::http::StatusCode::BAD_REQUEST,
serde_json::json!({
"error": format!("Invalid request body: {e}")
})
.to_string(),
)
.into_response()
}
}
}
#[cfg(feature = "axum")]
async fn login_handler(
State(_auth): State<Arc<AuthService>>,
Json(_body): Json<serde_json::Value>,
) -> Response {
log::info!("Received /login request: {_body}");
#[derive(Deserialize)]
struct LoginRequest {
username: String,
password: String,
}
let req: Result<LoginRequest, _> = serde_json::from_value(_body);
match req {
Ok(login) => {
log::info!(
"Attempting login for user: {username}",
username = login.username
);
match _auth
.login(crate::auth_service::LoginMethod::Credentials {
identifier: login.username,
password: login.password,
})
.await
{
Ok((user, tokens)) => {
log::info!("Login successful for user_id: {id}", id = user.id);
serde_json::json!({
"id": user.id,
"identifier": user.credentials.as_ref().map(|c| &c.identifier).unwrap_or(&"".to_string()),
"access_token": tokens.access_token,
"refresh_token": tokens.refresh_token
})
.to_string().into_response()
}
Err(e) => {
log::error!("Login failed: {e}");
(
axum::http::StatusCode::BAD_REQUEST,
serde_json::json!({
"error": e.to_string()
})
.to_string(),
)
.into_response()
}
}
}
Err(e) => {
log::error!("Invalid login request body: {e}");
(
axum::http::StatusCode::BAD_REQUEST,
serde_json::json!({
"error": format!("Invalid request body: {e}")
})
.to_string(),
)
.into_response()
}
}
}
#[cfg(feature = "axum")]
async fn health_handler() -> String {
log::info!("Received /health request");
"OK".to_string()
}
#[cfg(feature = "axum")]
async fn refresh_token_handler(
State(_auth): State<Arc<AuthService>>,
Json(_body): Json<serde_json::Value>,
) -> Response {
log::info!("Received /token/refresh request: {_body}");
#[derive(Deserialize)]
struct RefreshRequest {
refresh_token: String,
}
let req: Result<RefreshRequest, _> = serde_json::from_value(_body);
match req {
Ok(refresh) => {
log::info!("Attempting token refresh");
match _auth.refresh_access_token(&refresh.refresh_token).await {
Ok(tokens) => {
log::info!("Token refresh successful");
serde_json::json!({
"access_token": tokens.access_token,
"refresh_token": tokens.refresh_token
})
.to_string()
.into_response()
}
Err(e) => {
log::error!("Token refresh failed: {e}");
(
axum::http::StatusCode::BAD_REQUEST,
serde_json::json!({
"error": e.to_string()
})
.to_string(),
)
.into_response()
}
}
}
Err(e) => {
log::error!("Invalid refresh token request body: {e}");
(
axum::http::StatusCode::BAD_REQUEST,
serde_json::json!({
"error": format!("Invalid request body: {}", e)
})
.to_string(),
)
.into_response()
}
}
}
#[cfg(feature = "axum")]
async fn validate_token_handler(
State(_auth): State<Arc<AuthService>>,
Json(_body): Json<serde_json::Value>,
) -> Response {
log::info!("Received /token/validate request: {_body}");
#[derive(Deserialize)]
struct ValidateRequest {
token: String,
}
let req: Result<ValidateRequest, _> = serde_json::from_value(_body);
match req {
Ok(validate) => {
log::info!("Validating access token");
match _auth.validate_access_token(&validate.token).await {
Ok(claims) => {
log::info!(
"Token valid for subject: {subject}",
subject = claims.get_subject()
);
serde_json::json!({
"valid": true,
"subject": claims.get_subject(),
"expiration": claims.get_expiration()
})
.to_string()
.into_response()
}
Err(e) => {
log::error!("Token validation failed: {e}");
(
axum::http::StatusCode::BAD_REQUEST,
serde_json::json!({
"valid": false,
"error": e.to_string()
})
.to_string(),
)
.into_response()
}
}
}
Err(e) => {
log::error!("Invalid validate token request body: {e}");
(
axum::http::StatusCode::BAD_REQUEST,
serde_json::json!({
"error": format!("Invalid request body: {}", e)
})
.to_string(),
)
.into_response()
}
}
}
#[cfg(feature = "axum")]
async fn oauth_auth_handler(
State(_auth): State<Arc<AuthService>>,
Path(provider_str): Path<String>,
Query(params): Query<std::collections::HashMap<String, String>>,
) -> Response {
log::info!("Received /oauth/{provider_str}/auth request with params: {params:?}");
let provider = match provider_str.to_lowercase().as_str() {
"google" => crate::core::oauth::store::OAuth2Provider::Google,
"github" => crate::core::oauth::store::OAuth2Provider::GitHub,
"discord" => crate::core::oauth::store::OAuth2Provider::Discord,
"microsoft" => crate::core::oauth::store::OAuth2Provider::Microsoft,
_ => {
return (
axum::http::StatusCode::BAD_REQUEST,
serde_json::json!({
"error": format!("Unsupported OAuth2 provider: {}", provider_str)
})
.to_string(),
)
.into_response();
}
};
let state = match params.get("state") {
Some(state) => state,
None => {
return (
axum::http::StatusCode::BAD_REQUEST,
serde_json::json!({
"error": "Missing required 'state' parameter"
})
.to_string(),
)
.into_response();
}
};
let scopes = params
.get("scopes")
.map(|s| s.split(',').map(|scope| scope.trim().to_string()).collect());
match _auth
.generate_oauth2_auth_url(provider, state, scopes)
.await
{
Ok(auth_url) => {
log::info!("Generated OAuth2 auth URL for provider: {provider_str}");
serde_json::json!({
"auth_url": auth_url
})
.to_string()
.into_response()
}
Err(e) => {
log::error!("OAuth2 auth URL generation failed: {e}");
(
axum::http::StatusCode::BAD_REQUEST,
serde_json::json!({
"error": e.to_string()
})
.to_string(),
)
.into_response()
}
}
}
#[cfg(feature = "axum")]
async fn oauth_callback_handler(
State(_auth): State<Arc<AuthService>>,
Path(provider_str): Path<String>,
Query(params): Query<std::collections::HashMap<String, String>>,
) -> Response {
log::info!("Received /oauth/{provider_str}/callback request with params: {params:?}");
let provider = match provider_str.to_lowercase().as_str() {
"google" => crate::core::oauth::store::OAuth2Provider::Google,
"github" => crate::core::oauth::store::OAuth2Provider::GitHub,
"discord" => crate::core::oauth::store::OAuth2Provider::Discord,
"microsoft" => crate::core::oauth::store::OAuth2Provider::Microsoft,
_ => {
return (
axum::http::StatusCode::BAD_REQUEST,
serde_json::json!({
"error": format!("Unsupported OAuth2 provider: {}", provider_str)
})
.to_string(),
)
.into_response();
}
};
let code = match params.get("code") {
Some(code) => code,
None => {
return (
axum::http::StatusCode::BAD_REQUEST,
serde_json::json!({
"error": "Missing required 'code' parameter"
})
.to_string(),
)
.into_response();
}
};
let state = match params.get("state") {
Some(state) => state,
None => {
return (
axum::http::StatusCode::BAD_REQUEST,
serde_json::json!({
"error": "Missing required 'state' parameter"
})
.to_string(),
)
.into_response();
}
};
let frontend_uri = match _auth.get_oauth2_redirect_frontend_uri(provider).await {
Ok(uri) => uri,
Err(e) => {
log::error!("Failed to get frontend redirect URI: {e}");
return (
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
serde_json::json!({
"error": format!("Configuration error: {e}")
})
.to_string(),
)
.into_response();
}
};
match _auth
.login(crate::auth_service::LoginMethod::OAuth2 {
provider,
code: code.clone(),
state: state.clone(),
})
.await
{
Ok((user, tokens)) => {
log::info!(
"OAuth2 callback login successful for user_id: {id}",
id = user.id
);
let redirect_url = format!(
"{}#access_token={}&refresh_token={}&user_id={}&token_type=Bearer&expires_in=3600",
frontend_uri,
url_encode(&tokens.access_token),
url_encode(&tokens.refresh_token),
url_encode(&user.id)
);
log::info!("Redirecting user to frontend: {redirect_url}");
axum::response::Redirect::permanent(&redirect_url).into_response()
}
Err(e) => {
log::error!("OAuth2 callback login failed: {e}");
let error_redirect_url = format!(
"{}#error={}&error_description={}",
frontend_uri,
url_encode("authentication_failed"),
url_encode(&e.to_string())
);
log::info!("Redirecting user to frontend with error: {error_redirect_url}");
axum::response::Redirect::permanent(&error_redirect_url).into_response()
}
}
}
#[cfg(feature = "axum")]
async fn oauth_signup_handler(
State(_auth): State<Arc<AuthService>>,
Json(_body): Json<serde_json::Value>,
) -> Response {
log::info!("Received /oauth/signup request: {_body}");
#[derive(Deserialize)]
struct OAuth2SignupRequest {
provider: String,
code: String,
state: String,
}
let req: Result<OAuth2SignupRequest, _> = serde_json::from_value(_body);
match req {
Ok(signup) => {
log::info!(
"Attempting OAuth2 signup for provider: {provider}",
provider = signup.provider
);
let provider = match signup.provider.to_lowercase().as_str() {
"google" => crate::core::oauth::store::OAuth2Provider::Google,
"github" => crate::core::oauth::store::OAuth2Provider::GitHub,
"discord" => crate::core::oauth::store::OAuth2Provider::Discord,
"microsoft" => crate::core::oauth::store::OAuth2Provider::Microsoft,
_ => {
log::error!(
"Unsupported OAuth2 provider: {provider}",
provider = signup.provider
);
return (
axum::http::StatusCode::BAD_REQUEST,
serde_json::json!({
"error": format!("Unsupported OAuth2 provider: {}", signup.provider)
})
.to_string(),
)
.into_response();
}
};
match _auth
.signup(crate::auth_service::SignupMethod::OAuth2 {
provider,
code: signup.code,
state: signup.state,
})
.await
{
Ok((user, tokens)) => {
log::info!("OAuth2 signup successful for user_id: {id}", id = user.id);
serde_json::json!({
"id": user.id,
"access_token": tokens.access_token,
"refresh_token": tokens.refresh_token
})
.to_string()
.into_response()
}
Err(e) => {
log::error!("OAuth2 signup failed: {e}");
(
axum::http::StatusCode::BAD_REQUEST,
serde_json::json!({
"error": e.to_string()
})
.to_string(),
)
.into_response()
}
}
}
Err(e) => {
log::error!("Invalid OAuth2 signup request body: {e}");
(
axum::http::StatusCode::BAD_REQUEST,
serde_json::json!({
"error": format!("Invalid request body: {}", e)
})
.to_string(),
)
.into_response()
}
}
}
#[cfg(feature = "axum")]
async fn oauth_login_handler(
State(_auth): State<Arc<AuthService>>,
Json(_body): Json<serde_json::Value>,
) -> Response {
log::info!("Received /oauth/login request: {_body}");
#[derive(Deserialize)]
struct OAuth2LoginRequest {
provider: String,
code: String,
state: String,
}
let req: Result<OAuth2LoginRequest, _> = serde_json::from_value(_body);
match req {
Ok(login) => {
log::info!(
"Attempting OAuth2 login for provider: {provider}",
provider = login.provider
);
let provider = match login.provider.to_lowercase().as_str() {
"google" => crate::core::oauth::store::OAuth2Provider::Google,
"github" => crate::core::oauth::store::OAuth2Provider::GitHub,
"discord" => crate::core::oauth::store::OAuth2Provider::Discord,
"microsoft" => crate::core::oauth::store::OAuth2Provider::Microsoft,
_ => {
log::error!(
"Unsupported OAuth2 provider: {provider}",
provider = login.provider
);
return (
axum::http::StatusCode::BAD_REQUEST,
serde_json::json!({
"error": format!("Unsupported OAuth2 provider: {}", login.provider)
})
.to_string(),
)
.into_response();
}
};
match _auth
.login(crate::auth_service::LoginMethod::OAuth2 {
provider,
code: login.code,
state: login.state,
})
.await
{
Ok((user, tokens)) => {
log::info!("OAuth2 login successful for user_id: {id}", id = user.id);
serde_json::json!({
"id": user.id,
"access_token": tokens.access_token,
"refresh_token": tokens.refresh_token
})
.to_string()
.into_response()
}
Err(e) => {
log::error!("OAuth2 login failed: {e}");
(
axum::http::StatusCode::BAD_REQUEST,
serde_json::json!({
"error": e.to_string()
})
.to_string(),
)
.into_response()
}
}
}
Err(e) => {
log::error!("Invalid OAuth2 login request body: {e}");
(
axum::http::StatusCode::BAD_REQUEST,
serde_json::json!({
"error": format!("Invalid request body: {}", e)
})
.to_string(),
)
.into_response()
}
}
}