Fix panic-on-missing-env in auth middleware and ticket provider

require_auth and NxtgaugeTicketProvider::create_ticket both called
env::var(...).expect(...) on every request, crashing instead of
returning a proper error when JWT_SECRET/AI_SERVICE_KEY were unset.
Both are now loaded once into AppConfig at startup and checked with
a graceful 401/500 response.
This commit is contained in:
Ashwin Kumar Sivakumar 2026-07-02 18:31:35 +05:30
parent 2e0b986e20
commit 6decf8dce2
6 changed files with 55 additions and 10 deletions

View file

@ -10,3 +10,10 @@ OLLAMA_EMBED_MODEL=nomic-embed-text
HELP_CENTER_SEED_PATH=./seeds/help_articles.json
TICKETS_SOURCE=chatbot
NXTGAUGE_USERS_URL=http://localhost:9101
# Required: must match the JWT signing secret used by nxtgauge-backend-rust,
# and the AI service key nxtgauge-backend-rust expects on X-AI-Service-Key.
# Requests fail closed (401/500) if these are unset, rather than crashing.
JWT_SECRET=
AI_SERVICE_KEY=

View file

@ -1,5 +1,5 @@
use axum::{
extract::Request,
extract::{Request, State},
http::{header::AUTHORIZATION, StatusCode},
middleware::Next,
response::{IntoResponse, Response},
@ -8,6 +8,8 @@ use axum::{
use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation};
use serde::{Deserialize, Serialize};
use crate::state::AppState;
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Claims {
pub sub: String,
@ -25,7 +27,23 @@ pub struct AuthUser {
pub claims: Claims,
}
pub async fn require_auth(mut request: Request, next: Next) -> Response {
pub async fn require_auth(
State(state): State<AppState>,
mut request: Request,
next: Next,
) -> Response {
if state.config.jwt_secret.is_empty() {
tracing::error!("rejecting request: JWT_SECRET is not configured");
return (
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({
"error": "Authentication is misconfigured",
"code": "AUTH_MISCONFIGURED"
})),
)
.into_response();
}
let auth_header = request
.headers()
.get(AUTHORIZATION)
@ -46,11 +64,9 @@ pub async fn require_auth(mut request: Request, next: Next) -> Response {
}
};
let jwt_secret = std::env::var("JWT_SECRET").expect("JWT_SECRET must be set");
let token_data = match decode::<Claims>(
&token,
&DecodingKey::from_secret(jwt_secret.as_bytes()),
&DecodingKey::from_secret(state.config.jwt_secret.as_bytes()),
&Validation::new(Algorithm::HS256),
) {
Ok(data) => data,

View file

@ -15,6 +15,8 @@ pub struct AppConfig {
pub help_center_seed_path: String,
pub tickets_source: String,
pub nxtgauge_users_url: String,
pub jwt_secret: String,
pub ai_service_key: String,
}
impl AppConfig {
@ -47,6 +49,20 @@ impl AppConfig {
"NXTGAUGE_USERS_URL",
"http://nxtgauge-rust-users:9101",
),
jwt_secret: {
let v = std::env::var("JWT_SECRET").unwrap_or_default();
if v.is_empty() {
tracing::error!("JWT_SECRET is not set; all authenticated requests will be rejected");
}
v
},
ai_service_key: {
let v = std::env::var("AI_SERVICE_KEY").unwrap_or_default();
if v.is_empty() {
tracing::error!("AI_SERVICE_KEY is not set; ticket creation will fail");
}
v
},
}
}

View file

@ -85,6 +85,7 @@ async fn main() {
http_client,
cfg.nxtgauge_users_url.clone(),
cfg.tickets_source.clone(),
cfg.ai_service_key.clone(),
));
let state = AppState::new(

View file

@ -12,6 +12,7 @@ pub struct NxtgaugeTicketProvider {
client: Client,
base_url: String,
source: String,
ai_service_key: String,
}
#[derive(Debug, Deserialize)]
@ -31,11 +32,12 @@ struct NxtgaugeTicketRequest {
}
impl NxtgaugeTicketProvider {
pub fn new(client: Client, base_url: String, source: String) -> Self {
pub fn new(client: Client, base_url: String, source: String, ai_service_key: String) -> Self {
Self {
client,
base_url,
source,
ai_service_key,
}
}
}
@ -56,13 +58,16 @@ impl TicketProvider for NxtgaugeTicketProvider {
user_id: Some(payload.user_id),
};
let ai_service_key = std::env::var("AI_SERVICE_KEY")
.expect("AI_SERVICE_KEY must be set");
if self.ai_service_key.is_empty() {
return Err(AppError::ExternalService(
"AI_SERVICE_KEY is not configured".to_string(),
));
}
let response = self
.client
.post(&url)
.header("X-AI-Service-Key", ai_service_key)
.header("X-AI-Service-Key", &self.ai_service_key)
.json(&req)
.send()
.await

View file

@ -44,7 +44,7 @@ pub fn build_router(state: AppState) -> Router {
.route("/tickets/create", post(handlers::tickets::create))
.route("/help/search", post(handlers::help::search))
.route("/actions/confirm", post(handlers::confirm_action::confirm_action))
.layer(middleware::from_fn(require_auth)),
.layer(middleware::from_fn_with_state(state.clone(), require_auth)),
)
.layer(cors)
.layer(TraceLayer::new_for_http())