Files
OutFleet/src/web/handlers/subscription.rs

110 lines
4.6 KiB
Rust
Raw Normal View History

2025-10-19 05:06:38 +03:00
use axum::{
extract::{Path, State},
2025-10-19 15:23:17 +03:00
http::{StatusCode, HeaderMap, HeaderValue},
2025-10-19 05:06:38 +03:00
response::{IntoResponse, Response},
};
2025-10-19 05:27:55 +03:00
use base64::{Engine, engine::general_purpose};
2025-10-19 05:06:38 +03:00
use uuid::Uuid;
use crate::{
database::repository::{UserRepository, InboundUsersRepository},
services::uri_generator::UriGeneratorService,
web::AppState,
};
/// Get subscription links for a user by their ID
/// Returns all configuration links for the user, one per line
2025-10-19 15:23:17 +03:00
/// Based on Django implementation for compatibility
2025-10-19 05:06:38 +03:00
pub async fn get_user_subscription(
State(state): State<AppState>,
Path(user_id): Path<Uuid>,
) -> Result<Response, StatusCode> {
let user_repo = UserRepository::new(state.db.connection());
let inbound_users_repo = InboundUsersRepository::new(state.db.connection().clone());
// Check if user exists
let user = match user_repo.get_by_id(user_id).await {
Ok(Some(user)) => user,
Ok(None) => return Err(StatusCode::NOT_FOUND),
Err(_) => return Err(StatusCode::INTERNAL_SERVER_ERROR),
};
2025-10-19 15:23:17 +03:00
// Get all client config data for the user (this gets all active inbound accesses)
let all_configs = match inbound_users_repo.get_all_client_configs_for_user(user_id).await {
Ok(configs) => configs,
Err(e) => {
tracing::error!("Failed to get client configs for user {}: {}", user_id, e);
return Err(StatusCode::INTERNAL_SERVER_ERROR);
}
2025-10-19 05:06:38 +03:00
};
2025-10-19 15:23:17 +03:00
if all_configs.is_empty() {
2025-10-19 05:27:55 +03:00
let response_text = "# No configurations available\n".to_string();
let response_base64 = general_purpose::STANDARD.encode(response_text);
2025-10-19 05:06:38 +03:00
return Ok((
StatusCode::OK,
[("content-type", "text/plain; charset=utf-8")],
2025-10-19 05:27:55 +03:00
response_base64,
2025-10-19 05:06:38 +03:00
).into_response());
}
let mut config_lines = Vec::new();
2025-10-19 15:23:17 +03:00
// Generate connection strings for each config using existing UriGeneratorService
let uri_generator = UriGeneratorService::new();
for config_data in all_configs {
match uri_generator.generate_client_config(user_id, &config_data) {
Ok(client_config) => {
config_lines.push(client_config.uri);
tracing::debug!("Generated {} config for user {}: {}",
config_data.protocol.to_uppercase(), user.name, config_data.template_name);
2025-10-19 05:06:38 +03:00
}
Err(e) => {
2025-10-19 15:23:17 +03:00
tracing::warn!("Failed to generate connection string for user {} template {}: {}",
user.name, config_data.template_name, e);
2025-10-19 05:06:38 +03:00
continue;
}
}
}
if config_lines.is_empty() {
2025-10-19 05:27:55 +03:00
let response_text = "# No valid configurations available\n".to_string();
let response_base64 = general_purpose::STANDARD.encode(response_text);
2025-10-19 05:06:38 +03:00
return Ok((
StatusCode::OK,
[("content-type", "text/plain; charset=utf-8")],
2025-10-19 05:27:55 +03:00
response_base64,
2025-10-19 05:06:38 +03:00
).into_response());
}
2025-10-19 15:23:17 +03:00
// Join all URIs with newlines (like Django implementation)
2025-10-19 05:27:55 +03:00
let response_text = config_lines.join("\n") + "\n";
2025-10-19 15:23:17 +03:00
// Encode the entire response in base64 (like Django implementation)
2025-10-19 05:27:55 +03:00
let response_base64 = general_purpose::STANDARD.encode(response_text);
2025-10-19 05:06:38 +03:00
2025-10-19 15:23:17 +03:00
// Build response with subscription headers (like Django)
let mut headers = HeaderMap::new();
// Add headers required by VPN clients
headers.insert("content-type", HeaderValue::from_static("text/plain; charset=utf-8"));
headers.insert("content-disposition", HeaderValue::from_str(&format!("attachment; filename=\"{}\"", user.name)).unwrap());
headers.insert("cache-control", HeaderValue::from_static("no-cache"));
// Profile information
let profile_title = general_purpose::STANDARD.encode("OutFleet VPN");
headers.insert("profile-title", HeaderValue::from_str(&format!("base64:{}", profile_title)).unwrap());
headers.insert("profile-update-interval", HeaderValue::from_static("24"));
headers.insert("profile-web-page-url", HeaderValue::from_str(&format!("{}/u/{}", state.config.web.base_url, user_id)).unwrap());
headers.insert("support-url", HeaderValue::from_str(&format!("{}/admin/", state.config.web.base_url)).unwrap());
// Subscription info (unlimited service)
let expire_timestamp = chrono::Utc::now().timestamp() + (365 * 24 * 60 * 60); // 1 year from now
headers.insert("subscription-userinfo",
HeaderValue::from_str(&format!("upload=0; download=0; total=1099511627776; expire={}", expire_timestamp)).unwrap());
Ok((StatusCode::OK, headers, response_base64).into_response())
}