use super::models::{EnvVarResponse, EnvVarValueResponse, EnvVarsResponse, SetEnvVarRequest};
use crate::db::models::User;
use crate::db::{env_vars as db_env_vars, projects};
use crate::server::deployment::models as deployment_models;
use crate::server::extensions::InjectedEnvVarValue;
use crate::server::state::AppState;
use axum::{
extract::{Extension, Path, Query, State},
http::StatusCode,
Json,
};
use std::collections::HashMap;
fn format_error_chain(error: &anyhow::Error) -> String {
let mut chain = vec![error.to_string()];
let mut current_error = error.source();
while let Some(cause) = current_error {
chain.push(cause.to_string());
current_error = cause.source();
}
chain.join(" -> ")
}
async fn ensure_project_access_or_admin(
state: &AppState,
user: &User,
project: &crate::db::models::Project,
) -> Result<(), (StatusCode, String)> {
if state.is_admin(&user.email) {
return Ok(());
}
let can_access = projects::user_can_access(&state.db_pool, project.id, user.id)
.await
.map_err(|e| {
tracing::error!("Failed to check project access: {:#}", e);
(
StatusCode::INTERNAL_SERVER_ERROR,
"Database error".to_string(),
)
})?;
if !can_access {
return Err((
StatusCode::FORBIDDEN,
"You do not have access to this project".to_string(),
));
}
Ok(())
}
pub async fn set_project_env_var(
State(state): State<AppState>,
Extension(user): Extension<User>,
Path((project_id_or_name, key)): Path<(String, String)>,
Json(payload): Json<SetEnvVarRequest>,
) -> Result<Json<EnvVarResponse>, (StatusCode, String)> {
let project = if let Ok(uuid) = project_id_or_name.parse() {
projects::find_by_id(&state.db_pool, uuid)
.await
.map_err(|e| {
(
StatusCode::INTERNAL_SERVER_ERROR,
format!("Failed to get project: {}", e),
)
})?
} else {
projects::find_by_name(&state.db_pool, &project_id_or_name)
.await
.map_err(|e| {
(
StatusCode::INTERNAL_SERVER_ERROR,
format!("Failed to get project: {}", e),
)
})?
}
.ok_or_else(|| (StatusCode::NOT_FOUND, "Project not found".to_string()))?;
ensure_project_access_or_admin(&state, &user, &project).await?;
let is_protected = payload.is_protected.unwrap_or(payload.is_secret);
if is_protected && !payload.is_secret {
return Err((
StatusCode::BAD_REQUEST,
"Non-secret variables cannot be protected. Protection only applies to secrets."
.to_string(),
));
}
if payload.is_secret && state.encryption_provider.is_none() {
return Err((
StatusCode::BAD_REQUEST,
"Cannot store secret variables: no encryption provider configured".to_string(),
));
}
let value_to_store = if payload.is_secret {
let provider = state
.encryption_provider
.as_ref()
.expect("Encryption provider checked above");
provider.encrypt(&payload.value).await.map_err(|e| {
tracing::error!("Encryption failed: {:?}", e);
let error_chain = format_error_chain(&e);
(
StatusCode::INTERNAL_SERVER_ERROR,
format!("Failed to encrypt secret: {}", error_chain),
)
})?
} else {
payload.value.clone()
};
let env_var = db_env_vars::upsert_project_env_var(
&state.db_pool,
project.id,
&key,
&value_to_store,
payload.is_secret,
is_protected,
)
.await
.map_err(|e| {
(
StatusCode::INTERNAL_SERVER_ERROR,
format!("Failed to store environment variable: {}", e),
)
})?;
tracing::info!(
"Set environment variable '{}' for project '{}' (secret: {}, protected: {}). This will apply to new deployments only.",
key,
project.name,
payload.is_secret,
is_protected
);
Ok(Json(EnvVarResponse::from_db_model(
env_var.key,
env_var.value,
env_var.is_secret,
env_var.is_protected,
)))
}
pub async fn list_project_env_vars(
State(state): State<AppState>,
Extension(user): Extension<User>,
Path(project_id_or_name): Path<String>,
Query(params): Query<HashMap<String, String>>,
) -> Result<Json<EnvVarsResponse>, (StatusCode, String)> {
let project = if let Ok(uuid) = project_id_or_name.parse() {
projects::find_by_id(&state.db_pool, uuid)
.await
.map_err(|e| {
(
StatusCode::INTERNAL_SERVER_ERROR,
format!("Failed to get project: {}", e),
)
})?
} else {
projects::find_by_name(&state.db_pool, &project_id_or_name)
.await
.map_err(|e| {
(
StatusCode::INTERNAL_SERVER_ERROR,
format!("Failed to get project: {}", e),
)
})?
}
.ok_or_else(|| (StatusCode::NOT_FOUND, "Project not found".to_string()))?;
ensure_project_access_or_admin(&state, &user, &project).await?;
let include_unprotected = params
.get("include_unprotected_values")
.map(|v| v == "true")
.unwrap_or(false);
let db_env_vars = db_env_vars::list_project_env_vars(&state.db_pool, project.id)
.await
.map_err(|e| {
(
StatusCode::INTERNAL_SERVER_ERROR,
format!("Failed to list environment variables: {}", e),
)
})?;
let mut env_vars = Vec::new();
for var in db_env_vars {
let value = if include_unprotected && var.is_secret && !var.is_protected {
match &state.encryption_provider {
Some(provider) => provider.decrypt(&var.value).await.map_err(|e| {
tracing::error!(
"Failed to decrypt unprotected secret '{}': {:?}",
var.key,
e
);
(
StatusCode::INTERNAL_SERVER_ERROR,
format!("Failed to decrypt secret '{}': {}", var.key, e),
)
})?,
None => {
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
"Cannot decrypt secrets: no encryption provider configured".to_string(),
))
}
}
} else {
var.value.clone()
};
env_vars.push(
if var.is_secret && (!include_unprotected || var.is_protected) {
EnvVarResponse::from_db_model(var.key, var.value, var.is_secret, var.is_protected)
} else {
EnvVarResponse {
key: var.key,
value,
is_secret: var.is_secret,
is_protected: var.is_protected,
}
},
);
}
Ok(Json(EnvVarsResponse { env_vars }))
}
pub async fn delete_project_env_var(
State(state): State<AppState>,
Extension(user): Extension<User>,
Path((project_id_or_name, key)): Path<(String, String)>,
) -> Result<StatusCode, (StatusCode, String)> {
let project = if let Ok(uuid) = project_id_or_name.parse() {
projects::find_by_id(&state.db_pool, uuid)
.await
.map_err(|e| {
(
StatusCode::INTERNAL_SERVER_ERROR,
format!("Failed to get project: {}", e),
)
})?
} else {
projects::find_by_name(&state.db_pool, &project_id_or_name)
.await
.map_err(|e| {
(
StatusCode::INTERNAL_SERVER_ERROR,
format!("Failed to get project: {}", e),
)
})?
}
.ok_or_else(|| (StatusCode::NOT_FOUND, "Project not found".to_string()))?;
ensure_project_access_or_admin(&state, &user, &project).await?;
let deleted = db_env_vars::delete_project_env_var(&state.db_pool, project.id, &key)
.await
.map_err(|e| {
(
StatusCode::INTERNAL_SERVER_ERROR,
format!("Failed to delete environment variable: {}", e),
)
})?;
if !deleted {
return Err((
StatusCode::NOT_FOUND,
format!("Environment variable '{}' not found", key),
));
}
tracing::info!(
"Deleted environment variable '{}' from project '{}'. This will apply to new deployments only.",
key,
project.name
);
Ok(StatusCode::NO_CONTENT)
}
pub async fn list_deployment_env_vars(
State(state): State<AppState>,
Extension(user): Extension<User>,
Path((project_id_or_name, deployment_id)): Path<(String, String)>,
Query(params): Query<HashMap<String, String>>,
) -> Result<Json<EnvVarsResponse>, (StatusCode, String)> {
let project = if let Ok(uuid) = project_id_or_name.parse() {
projects::find_by_id(&state.db_pool, uuid)
.await
.map_err(|e| {
(
StatusCode::INTERNAL_SERVER_ERROR,
format!("Failed to get project: {}", e),
)
})?
} else {
projects::find_by_name(&state.db_pool, &project_id_or_name)
.await
.map_err(|e| {
(
StatusCode::INTERNAL_SERVER_ERROR,
format!("Failed to get project: {}", e),
)
})?
}
.ok_or_else(|| (StatusCode::NOT_FOUND, "Project not found".to_string()))?;
ensure_project_access_or_admin(&state, &user, &project).await?;
let deployment =
crate::db::deployments::find_by_deployment_id(&state.db_pool, &deployment_id, project.id)
.await
.map_err(|e| {
(
StatusCode::INTERNAL_SERVER_ERROR,
format!("Failed to get deployment: {}", e),
)
})?
.ok_or_else(|| (StatusCode::NOT_FOUND, "Deployment not found".to_string()))?;
let include_unprotected = params
.get("include_unprotected_values")
.map(|v| v == "true")
.unwrap_or(false);
let db_env_vars = db_env_vars::list_deployment_env_vars(&state.db_pool, deployment.id)
.await
.map_err(|e| {
(
StatusCode::INTERNAL_SERVER_ERROR,
format!("Failed to list deployment environment variables: {}", e),
)
})?;
let mut env_vars = Vec::new();
for var in db_env_vars {
let value = if include_unprotected && var.is_secret && !var.is_protected {
match &state.encryption_provider {
Some(provider) => provider.decrypt(&var.value).await.map_err(|e| {
tracing::error!(
"Failed to decrypt unprotected secret '{}': {:?}",
var.key,
e
);
(
StatusCode::INTERNAL_SERVER_ERROR,
format!("Failed to decrypt secret '{}': {}", var.key, e),
)
})?,
None => {
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
"Cannot decrypt secrets: no encryption provider configured".to_string(),
))
}
}
} else {
var.value.clone()
};
env_vars.push(
if var.is_secret && (!include_unprotected || var.is_protected) {
EnvVarResponse::from_db_model(var.key, var.value, var.is_secret, var.is_protected)
} else {
EnvVarResponse {
key: var.key,
value,
is_secret: var.is_secret,
is_protected: var.is_protected,
}
},
);
}
Ok(Json(EnvVarsResponse { env_vars }))
}
pub async fn get_project_env_var_value(
State(state): State<AppState>,
Extension(user): Extension<User>,
Path((project_id_or_name, key)): Path<(String, String)>,
) -> Result<Json<EnvVarValueResponse>, (StatusCode, String)> {
let project = if let Ok(uuid) = project_id_or_name.parse() {
projects::find_by_id(&state.db_pool, uuid)
.await
.map_err(|e| {
(
StatusCode::INTERNAL_SERVER_ERROR,
format!("Failed to get project: {}", e),
)
})?
} else {
projects::find_by_name(&state.db_pool, &project_id_or_name)
.await
.map_err(|e| {
(
StatusCode::INTERNAL_SERVER_ERROR,
format!("Failed to get project: {}", e),
)
})?
}
.ok_or_else(|| (StatusCode::NOT_FOUND, "Project not found".to_string()))?;
ensure_project_access_or_admin(&state, &user, &project).await?;
let env_var = db_env_vars::get_project_env_var(&state.db_pool, project.id, &key)
.await
.map_err(|e| {
(
StatusCode::INTERNAL_SERVER_ERROR,
format!("Failed to get environment variable: {}", e),
)
})?
.ok_or_else(|| {
(
StatusCode::NOT_FOUND,
format!("Environment variable '{}' not found", key),
)
})?;
if !env_var.is_secret || env_var.is_protected {
return Err((
StatusCode::BAD_REQUEST,
format!(
"Environment variable '{}' is a protected secret and cannot be retrieved. \
Update it with --protected=false to allow retrieval.",
key
),
));
}
let decrypted_value = match &state.encryption_provider {
Some(provider) => provider.decrypt(&env_var.value).await.map_err(|e| {
tracing::error!("Failed to decrypt unprotected secret '{}': {:?}", key, e);
(
StatusCode::INTERNAL_SERVER_ERROR,
format!("Failed to decrypt secret '{}': {}", key, e),
)
})?,
None => {
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
"Cannot decrypt secrets: no encryption provider configured".to_string(),
))
}
};
tracing::info!(
"Retrieved decrypted value for secret '{}' in project '{}' by user '{}'",
key,
project.name,
user.email
);
Ok(Json(EnvVarValueResponse {
value: decrypted_value,
}))
}
pub async fn preview_deployment_env_vars(
State(state): State<AppState>,
Extension(user): Extension<User>,
Path(project_id_or_name): Path<String>,
Query(params): Query<HashMap<String, String>>,
) -> Result<Json<EnvVarsResponse>, (StatusCode, String)> {
let project = if let Ok(uuid) = project_id_or_name.parse() {
projects::find_by_id(&state.db_pool, uuid)
.await
.map_err(|e| {
(
StatusCode::INTERNAL_SERVER_ERROR,
format!("Failed to get project: {}", e),
)
})?
} else {
projects::find_by_name(&state.db_pool, &project_id_or_name)
.await
.map_err(|e| {
(
StatusCode::INTERNAL_SERVER_ERROR,
format!("Failed to get project: {}", e),
)
})?
}
.ok_or_else(|| (StatusCode::NOT_FOUND, "Project not found".to_string()))?;
ensure_project_access_or_admin(&state, &user, &project).await?;
let deployment_group = params
.get("deployment_group")
.cloned()
.unwrap_or_else(|| "default".to_string());
let mut env_map: HashMap<String, EnvVarResponse> = HashMap::new();
let db_vars = db_env_vars::list_project_env_vars(&state.db_pool, project.id)
.await
.map_err(|e| {
(
StatusCode::INTERNAL_SERVER_ERROR,
format!("Failed to list environment variables: {}", e),
)
})?;
for var in db_vars {
if var.is_secret && !var.is_protected {
let decrypted = match &state.encryption_provider {
Some(provider) => provider.decrypt(&var.value).await.map_err(|e| {
tracing::error!(
"Failed to decrypt unprotected secret '{}': {:?}",
var.key,
e
);
(
StatusCode::INTERNAL_SERVER_ERROR,
format!("Failed to decrypt secret '{}': {}", var.key, e),
)
})?,
None => {
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
"Cannot decrypt secrets: no encryption provider configured".to_string(),
))
}
};
env_map.insert(
var.key.clone(),
EnvVarResponse {
key: var.key,
value: decrypted,
is_secret: true,
is_protected: false,
},
);
} else if var.is_secret {
env_map.insert(
var.key.clone(),
EnvVarResponse {
key: var.key,
value: "••••••••".to_string(),
is_secret: true,
is_protected: true,
},
);
} else {
env_map.insert(
var.key.clone(),
EnvVarResponse {
key: var.key.clone(),
value: var.value,
is_secret: false,
is_protected: false,
},
);
}
}
if !env_map.contains_key("PORT") {
env_map.insert(
"PORT".to_string(),
EnvVarResponse {
key: "PORT".to_string(),
value: "8080".to_string(),
is_secret: false,
is_protected: false,
},
);
}
match state
.deployment_backend
.get_project_urls(&project, &deployment_group)
.await
{
Ok(urls) => {
for (key, value) in
deployment_models::rise_system_env_vars(&state.public_url, &deployment_group, &urls)
{
env_map.insert(
key.clone(),
EnvVarResponse {
key,
value,
is_secret: false,
is_protected: false,
},
);
}
}
Err(e) => {
tracing::debug!(
"Could not compute project URLs for preview (no deployment controller?): {:?}",
e
);
for (key, value) in [
("RISE_ISSUER", state.public_url.clone()),
("RISE_DEPLOYMENT_GROUP", deployment_group.clone()),
(
"RISE_DEPLOYMENT_GROUP_NORMALIZED",
deployment_models::normalize_deployment_group(&deployment_group),
),
] {
env_map.insert(
key.to_string(),
EnvVarResponse {
key: key.to_string(),
value,
is_secret: false,
is_protected: false,
},
);
}
}
}
for (_, extension) in state.extension_registry.iter() {
match extension
.preview_env_vars(project.id, &deployment_group)
.await
{
Ok(vars) => {
for var in vars {
let response = match var.value {
InjectedEnvVarValue::Plain(v) => EnvVarResponse {
key: var.key.clone(),
value: v,
is_secret: false,
is_protected: false,
},
InjectedEnvVarValue::Secret { decrypted, .. } => EnvVarResponse {
key: var.key.clone(),
value: decrypted,
is_secret: true,
is_protected: false,
},
InjectedEnvVarValue::Protected { .. } => EnvVarResponse {
key: var.key.clone(),
value: "••••••••".to_string(),
is_secret: true,
is_protected: true,
},
};
env_map.insert(var.key, response);
}
}
Err(e) => {
tracing::warn!(
"Extension '{}' failed to provide preview env vars: {:?}",
extension.extension_type(),
e
);
}
}
}
let mut env_vars: Vec<EnvVarResponse> = env_map.into_values().collect();
env_vars.sort_by(|a, b| a.key.cmp(&b.key));
Ok(Json(EnvVarsResponse { env_vars }))
}