use axum::extract::Extension;
use axum::http::StatusCode;
use axum::response::{IntoResponse, Json};
use bcrypt::{DEFAULT_COST, hash};
use tracing::instrument;
use uuid::Uuid;
use super::super::extractors::OAuthRepo;
use super::super::responses::{created_response, error_response};
use systemprompt_models::RequestContext;
use systemprompt_models::modules::ApiPaths;
use systemprompt_oauth::clients::api::{CreateOAuthClientRequest, OAuthClientResponse};
use systemprompt_oauth::repository::CreateClientParams;
#[instrument(skip(repository, req_ctx, request), fields(client_id = %request.client_id))]
pub async fn create_client(
Extension(req_ctx): Extension<RequestContext>,
OAuthRepo(repository): OAuthRepo,
Json(request): Json<CreateOAuthClientRequest>,
) -> impl IntoResponse {
let client_secret = Uuid::new_v4().to_string();
let client_secret_hash = match hash(&client_secret, DEFAULT_COST) {
Ok(hash) => hash,
Err(e) => {
tracing::error!(
error = %e,
client_id = %request.client_id,
created_by = %req_ctx.auth.user_id,
"OAuth client creation failed"
);
return error_response(
StatusCode::INTERNAL_SERVER_ERROR,
"server_error",
&format!("Failed to hash client secret: {e}"),
);
},
};
let params = CreateClientParams {
client_id: request.client_id.clone(),
client_secret_hash,
client_name: request.name.clone(),
redirect_uris: request.redirect_uris.clone(),
grant_types: Some(vec![
"authorization_code".to_string(),
"refresh_token".to_string(),
]),
response_types: Some(vec!["code".to_string()]),
scopes: request.scopes.clone(),
token_endpoint_auth_method: Some("client_secret_basic".to_string()),
client_uri: None,
logo_uri: None,
contacts: None,
};
match repository.create_client(params).await {
Ok(client) => {
tracing::info!(
client_id = %client.client_id,
client_name = ?client.name,
redirect_uris = ?request.redirect_uris,
scopes = ?request.scopes,
created_by = %req_ctx.auth.user_id,
"OAuth client created"
);
let location = ApiPaths::oauth_client_location(client.client_id.as_str());
let response: OAuthClientResponse = client.into();
match serde_json::to_value(response) {
Ok(mut response_json) => {
response_json["client_secret"] = serde_json::Value::String(client_secret);
created_response(response_json, location)
},
Err(e) => error_response(
StatusCode::INTERNAL_SERVER_ERROR,
"server_error",
&format!("Failed to serialize response: {e}"),
),
}
},
Err(e) => {
let error_msg = format!("Failed to create client: {e}");
let is_duplicate = error_msg.contains("UNIQUE constraint failed");
tracing::info!(
client_id = %request.client_id,
reason = if is_duplicate { "Client ID already exists" } else { &error_msg },
created_by = %req_ctx.auth.user_id,
"OAuth client creation rejected"
);
if is_duplicate {
error_response(
StatusCode::CONFLICT,
"conflict",
"Client with this ID already exists",
)
} else {
error_response(StatusCode::BAD_REQUEST, "bad_request", &error_msg)
}
},
}
}