Files
tuliprox/backend/src/model/config/api_proxy.rs
T
Juanjo PresaandGitHub 945308a65b feat: add Plex media-server catalog and playback baseline (#748)
* feat: add Plex media-server catalog baseline
* feat: add Plex media-server playback baseline
2026-05-11 13:38:06 +02:00

252 lines
9.9 KiB
Rust

use crate::model::{macros, AppConfig, Config, ProxyUserCredentials, TargetUser};
use crate::repository::{backup_api_user_db_file, get_api_user_db_path, load_api_user, merge_api_user};
use crate::utils;
use crate::utils::file_exists_async;
use arc_swap::access::Access;
use arc_swap::ArcSwap;
use log::debug;
use shared::model::{ApiProxyConfigDto, ApiProxyServerInfoDto, ConfigPaths, TargetUserDto};
use std::cmp::PartialEq;
use std::io::ErrorKind;
use std::sync::Arc;
const API_USER: &str = "api";
const TEST_USER: &str = "test";
#[derive(Debug, Clone)]
pub struct ApiProxyServerInfo {
pub name: String,
pub protocol: String,
pub host: String,
pub port: Option<String>,
pub timezone: String,
pub message: String,
pub path: Option<String>,
}
macros::from_impl!(ApiProxyServerInfo);
impl From<&ApiProxyServerInfoDto> for ApiProxyServerInfo {
fn from(dto: &ApiProxyServerInfoDto) -> Self {
Self {
name: dto.name.clone(),
protocol: dto.protocol.clone(),
host: dto.host.clone(),
port: dto.port.clone(),
timezone: dto.timezone.clone(),
message: dto.message.clone(),
path: dto.path.clone(),
}
}
}
impl From<&ApiProxyServerInfo> for ApiProxyServerInfoDto {
fn from(instance: &ApiProxyServerInfo) -> Self {
Self {
name: instance.name.clone(),
protocol: instance.protocol.clone(),
host: instance.host.clone(),
port: instance.port.clone(),
timezone: instance.timezone.clone(),
message: instance.message.clone(),
path: instance.path.clone(),
}
}
}
impl ApiProxyServerInfo {
pub fn get_base_url(&self) -> String {
let base_url = if let Some(port) = self.port.as_ref() {
format!("{}://{}:{port}", self.protocol, self.host)
} else {
format!("{}://{}", self.protocol, self.host)
};
match &self.path {
None => base_url,
Some(path) => format!("{base_url}/{}", path.trim_matches('/')),
}
}
}
#[derive(Debug, Clone, Default)]
pub struct ApiProxyConfig {
pub server: Vec<ApiProxyServerInfo>,
pub user: Vec<TargetUser>,
pub use_user_db: bool,
/// HTTP status code for auth failures. 0 means default (403).
pub auth_error_status: u16,
}
macros::from_impl!(ApiProxyConfig);
impl From<&ApiProxyConfigDto> for ApiProxyConfig {
fn from(dto: &ApiProxyConfigDto) -> Self {
Self {
server: dto.server.iter().map(ApiProxyServerInfo::from).collect(),
user: dto.user.iter().map(TargetUser::from).collect(),
use_user_db: dto.use_user_db,
auth_error_status: dto.auth_error_status,
}
}
}
impl From<&ApiProxyConfig> for ApiProxyConfigDto {
fn from(instance: &ApiProxyConfig) -> Self {
Self {
server: instance.server.iter().map(ApiProxyServerInfoDto::from).collect(),
user: instance.user.iter().map(TargetUserDto::from).collect(),
use_user_db: instance.use_user_db,
auth_error_status: instance.auth_error_status,
}
}
}
fn serialize_api_proxy_config(config: &ApiProxyConfigDto) -> Result<String, String> {
let mut serialized = String::new();
let options = serde_saphyr::ser_options! {prefer_block_scalars: false};
serde_saphyr::to_fmt_writer_with_options(&mut serialized, config, options)
.map_err(|err| format!("Could not serialize api proxy config: {err}"))?;
Ok(serialized)
}
async fn api_proxy_file_would_change(api_proxy_file: &str, config: &ApiProxyConfigDto) -> Result<bool, String> {
let serialized = serialize_api_proxy_config(config)?;
match tokio::fs::read_to_string(api_proxy_file).await {
Ok(existing) => Ok(existing != serialized),
Err(err) if err.kind() == ErrorKind::NotFound => Ok(true),
Err(err) => Err(format!("Could not read api proxy file {api_proxy_file}: {err}")),
}
}
impl ApiProxyConfig {
async fn backfill_output_clusters_to_file(&self, cfg: &AppConfig, errors: &mut Vec<String>) {
if self.user.is_empty() {
return;
}
let paths = <Arc<ArcSwap<ConfigPaths>> as Access<ConfigPaths>>::load(&cfg.paths);
let api_proxy_file = paths.api_proxy_file_path.as_str();
let dto = ApiProxyConfigDto::from(self);
match api_proxy_file_would_change(api_proxy_file, &dto).await {
Ok(true) => {}
Ok(false) => return,
Err(err) => {
errors.push(err);
return;
}
}
let config = <Arc<ArcSwap<Config>> as Access<Config>>::load(&cfg.config);
let backup_dir = config.get_backup_dir();
if let Err(err) = utils::save_api_proxy(api_proxy_file, backup_dir.as_ref(), &dto).await {
errors.push(format!("Error saving api proxy file: {err}"));
}
}
// we have the option to store user in the config file or in the user_db
// When we switch from one to other we need to migrate the existing data.
/// # Panics
pub async fn migrate_api_user(&mut self, cfg: &AppConfig, errors: &mut Vec<String>) {
let paths = <Arc<ArcSwap<ConfigPaths>> as Access<ConfigPaths>>::load(&cfg.paths);
let api_proxy_file = paths.api_proxy_file_path.as_str();
if self.use_user_db {
// we have user defined in config file.
// we migrate them to the db and delete them from the config file
if !&self.user.is_empty() {
if let Err(err) = merge_api_user(cfg, &self.user).await {
errors.push(err.to_string());
} else {
let config = <Arc<ArcSwap<Config>> as Access<Config>>::load(&cfg.config);
let backup_dir = config.get_backup_dir();
self.user = vec![];
if let Err(err) =
utils::save_api_proxy(api_proxy_file, backup_dir.as_ref(), &ApiProxyConfigDto::from(&*self))
.await
{
errors.push(format!("Error saving api proxy file: {err}"));
}
}
}
match load_api_user(cfg).await {
Ok(users) => {
self.user = users;
}
Err(err) => {
println!("{err}");
errors.push(err.to_string());
}
}
} else {
self.backfill_output_clusters_to_file(cfg, errors).await;
let user_db_path = get_api_user_db_path(cfg);
if file_exists_async(&user_db_path).await {
// we can't have user defined in db file.
// we need to load them and save them into the config file
if let Ok(stored_users) = load_api_user(cfg).await {
for stored_user in stored_users {
if let Some(target_user) = self.user.iter_mut().find(|t| t.target == stored_user.target) {
for stored_credential in &stored_user.credentials {
if !target_user.credentials.iter().any(|c| c.username == stored_credential.username) {
target_user.credentials.push(stored_credential.clone());
}
}
} else {
self.user.push(stored_user);
}
}
}
let config = <Arc<ArcSwap<Config>> as Access<Config>>::load(&cfg.config);
let backup_dir = config.get_backup_dir();
let dto = ApiProxyConfigDto::from(&*self);
match api_proxy_file_would_change(api_proxy_file, &dto).await {
Ok(true) => {
if let Err(err) = utils::save_api_proxy(api_proxy_file, backup_dir.as_ref(), &dto).await {
errors.push(format!("Error saving api proxy file: {err}"));
} else {
backup_api_user_db_file(cfg, &user_db_path).await;
let _ = tokio::fs::remove_file(&user_db_path).await;
}
}
Ok(false) => {
backup_api_user_db_file(cfg, &user_db_path).await;
let _ = tokio::fs::remove_file(&user_db_path).await;
}
Err(err) => errors.push(err),
}
}
}
}
pub fn get_target_name(&self, username: &str, password: &str) -> Option<(Arc<ProxyUserCredentials>, String)> {
for target_user in &self.user {
if let Some((credentials, target_name)) = target_user.get_target_name(username, password) {
return Some((Arc::clone(&credentials), target_name.to_string()));
}
}
if log::log_enabled!(log::Level::Debug) && !username.eq(API_USER) {
debug!("Could not find any target for user {username}");
}
None
}
pub fn get_target_name_by_token(&self, token: &str) -> Option<(Arc<ProxyUserCredentials>, String)> {
for target_user in &self.user {
if let Some((credentials, target_name)) = target_user.get_target_name_by_token(token) {
return Some((Arc::clone(&credentials), target_name.to_string()));
}
}
None
}
pub fn get_user_credentials(&self, username: &str) -> Option<Arc<ProxyUserCredentials>> {
let result = self
.user
.iter()
.flat_map(|target_user| target_user.credentials.iter())
.find(|credential| credential.username == username)
.map(Arc::clone);
if result.is_none() && (username != TEST_USER && username != API_USER) {
debug!("Could not find any user credentials for: {username}");
}
result
}
}