mirror of
https://github.com/euzu/tuliprox.git
synced 2026-10-16 12:52:24 +02:00
Merge pull request #374 from euzu/feature/user_edit_change_playlist
Changing playlist in user editor now possible Custom video response optimizatation
This commit is contained in:
@@ -1,11 +1,13 @@
|
||||
use std::path::PathBuf;
|
||||
use crate::api::model::AppState;
|
||||
use crate::model::ProxyUserCredentials;
|
||||
use crate::model::{ApiProxyConfig, ProxyUserCredentials, TargetUser};
|
||||
use crate::repository::user_repository::store_api_user;
|
||||
use axum::response::IntoResponse;
|
||||
use axum::Router;
|
||||
use serde_json::json;
|
||||
use shared::model::{ApiProxyConfigDto, ProxyUserCredentialsDto};
|
||||
use std::sync::Arc;
|
||||
use shared::utils::mask_credentials;
|
||||
|
||||
async fn save_config_api_proxy_user(
|
||||
method: axum::http::Method,
|
||||
@@ -13,76 +15,129 @@ async fn save_config_api_proxy_user(
|
||||
axum::extract::Path(target_name): axum::extract::Path<String>,
|
||||
axum::extract::Json(mut credential): axum::extract::Json<ProxyUserCredentialsDto>,
|
||||
) -> impl axum::response::IntoResponse + Send {
|
||||
let virtual_file = PathBuf::from("api_proxy");
|
||||
let _lock = app_state.app_config.file_locks.write_lock(&virtual_file).await;
|
||||
|
||||
credential.prepare();
|
||||
if let Err(err) = credential.validate() {
|
||||
return (axum::http::StatusCode::BAD_REQUEST, axum::Json(json!({"error": err.to_string()}))).into_response();
|
||||
return (
|
||||
axum::http::StatusCode::BAD_REQUEST,
|
||||
axum::Json(json!({"error": err.to_string()}))
|
||||
).into_response();
|
||||
}
|
||||
|
||||
let is_update = method == axum::http::Method::PUT;
|
||||
|
||||
if let Some(old_api_proxy) = app_state.app_config.api_proxy.load().clone() {
|
||||
let mut api_proxy = (*old_api_proxy).clone();
|
||||
let mut target_found = false;
|
||||
for target_user in &api_proxy.user {
|
||||
if target_user.target == target_name {
|
||||
target_found = true;
|
||||
}
|
||||
for user in &target_user.credentials {
|
||||
if !is_update && user.username == credential.username {
|
||||
return (axum::http::StatusCode::BAD_REQUEST, axum::Json(json!({"error": format!("Duplicate username {}", &credential.username)}))).into_response();
|
||||
}
|
||||
if let (Some(u), Some(c)) = (&user.token, &credential.token) {
|
||||
if u == c && user.username != credential.username {
|
||||
return (
|
||||
axum::http::StatusCode::BAD_REQUEST,
|
||||
axum::Json(json!({"error": format!("Duplicate token {c}")}))
|
||||
).into_response();
|
||||
}
|
||||
let mut api_proxy = if let Some(old) = app_state.app_config.api_proxy.load().clone() {
|
||||
(*old).clone()
|
||||
} else {
|
||||
ApiProxyConfig::default()
|
||||
};
|
||||
|
||||
// ---------- Search for existing Target and existing User ----------
|
||||
let mut existing_target_index: Option<usize> = None; // index of target (target_name), falls vorhanden
|
||||
let mut existing_user_target_index: Option<usize> = None; // index of existing users target
|
||||
let mut existing_user_index: Option<usize> = None; // index of the user in the targets credentials list
|
||||
|
||||
for (t_idx, target_user) in api_proxy.user.iter().enumerate() {
|
||||
if target_user.target == target_name {
|
||||
existing_target_index = Some(t_idx);
|
||||
}
|
||||
for (u_idx, user) in target_user.credentials.iter().enumerate() {
|
||||
if let (Some(u), Some(c)) = (&user.token, &credential.token) {
|
||||
if u == c && user.username != credential.username {
|
||||
return ( axum::http::StatusCode::BAD_REQUEST, axum::Json(json!({"error": format!("Duplicate token {}", mask_credentials(c))})) ).into_response();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if is_update && !target_found {
|
||||
return (axum::http::StatusCode::BAD_REQUEST, axum::Json(json!({"error": format!("Target not found {target_name}")}))).into_response();
|
||||
}
|
||||
|
||||
for target in &mut api_proxy.user {
|
||||
if target.target == target_name {
|
||||
if is_update {
|
||||
let mut updated = false;
|
||||
for user in &mut target.credentials {
|
||||
if user.username == credential.username {
|
||||
*user = ProxyUserCredentials::from(&credential);
|
||||
updated = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if !updated {
|
||||
return (axum::http::StatusCode::BAD_REQUEST, axum::Json(json!({"error": format!("User {} not found in target {target_name}", credential.username)}))).into_response();
|
||||
}
|
||||
} else {
|
||||
target.credentials.push(ProxyUserCredentials::from(&credential));
|
||||
if user.username == credential.username {
|
||||
// if not an update und username exists -> Error (duplicate username)
|
||||
if !is_update {
|
||||
return (
|
||||
axum::http::StatusCode::BAD_REQUEST,
|
||||
axum::Json(json!({"error": format!("Duplicate username {}", &credential.username)}))
|
||||
).into_response();
|
||||
}
|
||||
|
||||
// mark position of user (for update / move)
|
||||
existing_user_target_index = Some(t_idx);
|
||||
existing_user_index = Some(u_idx);
|
||||
}
|
||||
}
|
||||
|
||||
let new_api_proxy = Arc::new(api_proxy);
|
||||
|
||||
if new_api_proxy.use_user_db {
|
||||
if let Err(err) = store_api_user(&app_state.app_config, &new_api_proxy.user) {
|
||||
return (axum::http::StatusCode::INTERNAL_SERVER_ERROR, axum::Json(json!({"error": err.to_string()}))).into_response();
|
||||
}
|
||||
} else {
|
||||
let config = app_state.app_config.config.load();
|
||||
let backup_dir = config.get_backup_dir();
|
||||
let paths = app_state.app_config.paths.load();
|
||||
if let Some(err) = crate::api::endpoints::v1_api_config::intern_save_config_api_proxy(backup_dir.as_ref(), &ApiProxyConfigDto::from(&*new_api_proxy), paths.api_proxy_file_path.as_str()) {
|
||||
return (axum::http::StatusCode::INTERNAL_SERVER_ERROR, axum::Json(json!({"error": err.to_string()}))).into_response();
|
||||
}
|
||||
}
|
||||
// Udate state after successful save
|
||||
app_state.app_config.api_proxy.store(Some(Arc::clone(&new_api_proxy)));
|
||||
}
|
||||
|
||||
// ---------- if update but no user found -> Error ----------
|
||||
if is_update && existing_user_index.is_none() {
|
||||
return (
|
||||
axum::http::StatusCode::BAD_REQUEST,
|
||||
axum::Json(json!({"error": format!("User {} not found", credential.username)}))
|
||||
).into_response();
|
||||
}
|
||||
|
||||
|
||||
// ---------- create target if new target does not exist ----------
|
||||
let target_idx = if let Some(idx) = existing_target_index {
|
||||
idx
|
||||
} else {
|
||||
api_proxy.user.push(TargetUser {
|
||||
target: target_name.clone(),
|
||||
credentials: vec![],
|
||||
});
|
||||
api_proxy.user.len() - 1
|
||||
};
|
||||
|
||||
if is_update {
|
||||
let mut remove_empty_target = false;
|
||||
// existing_user_index and existing_user_target_index exists at this point
|
||||
let user_idx = existing_user_index.unwrap();
|
||||
let user_target_idx = existing_user_target_index.unwrap();
|
||||
|
||||
if user_target_idx == target_idx {
|
||||
// Update
|
||||
api_proxy.user[user_target_idx].credentials[user_idx] = ProxyUserCredentials::from(&credential);
|
||||
} else {
|
||||
// Move: remove from old target and insert into new target
|
||||
api_proxy.user[user_target_idx].credentials.remove(user_idx);
|
||||
api_proxy.user[target_idx].credentials.push(ProxyUserCredentials::from(&credential));
|
||||
remove_empty_target = api_proxy.user[user_target_idx].credentials.is_empty();
|
||||
}
|
||||
|
||||
if remove_empty_target {
|
||||
api_proxy.user.retain(|t| !t.credentials.is_empty());
|
||||
}
|
||||
|
||||
} else {
|
||||
// new user
|
||||
api_proxy.user[target_idx].credentials.push(ProxyUserCredentials::from(&credential));
|
||||
}
|
||||
|
||||
let new_api_proxy = Arc::new(api_proxy);
|
||||
|
||||
if new_api_proxy.use_user_db {
|
||||
if let Err(err) = store_api_user(&app_state.app_config, &new_api_proxy.user) {
|
||||
return (
|
||||
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||
axum::Json(json!({"error": err.to_string()}))
|
||||
).into_response();
|
||||
}
|
||||
} else {
|
||||
let config = app_state.app_config.config.load();
|
||||
let backup_dir = config.get_backup_dir();
|
||||
let paths = app_state.app_config.paths.load();
|
||||
if let Some(err) = crate::api::endpoints::v1_api_config::intern_save_config_api_proxy(
|
||||
backup_dir.as_ref(),
|
||||
&ApiProxyConfigDto::from(&*new_api_proxy),
|
||||
paths.api_proxy_file_path.as_str(),
|
||||
) {
|
||||
return (
|
||||
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||
axum::Json(json!({"error": err.to_string()}))
|
||||
).into_response();
|
||||
}
|
||||
}
|
||||
|
||||
// Update state after successful save
|
||||
app_state.app_config.api_proxy.store(Some(Arc::clone(&new_api_proxy)));
|
||||
|
||||
axum::http::StatusCode::OK.into_response()
|
||||
}
|
||||
|
||||
|
||||
@@ -1,19 +1,26 @@
|
||||
use crate::api::model::StreamError;
|
||||
use crate::api::model::TransportStreamBuffer;
|
||||
use crate::api::model::{StreamError, TransportStreamBuffer};
|
||||
use bytes::Bytes;
|
||||
use futures::Stream;
|
||||
use std::pin::Pin;
|
||||
use std::task::{Context, Poll};
|
||||
|
||||
use std::{
|
||||
pin::Pin,
|
||||
task::{Context, Poll},
|
||||
time::Duration,
|
||||
};
|
||||
use std::future::Future;
|
||||
use tokio::time::{Sleep, sleep};
|
||||
|
||||
pub struct CustomVideoStream {
|
||||
buffer: TransportStreamBuffer,
|
||||
delay: Option<Pin<Box<Sleep>>>,
|
||||
frame_interval: Duration,
|
||||
}
|
||||
|
||||
impl CustomVideoStream {
|
||||
pub fn new(buffer: TransportStreamBuffer) -> Self {
|
||||
Self {
|
||||
buffer
|
||||
buffer,
|
||||
delay: None,
|
||||
frame_interval: Duration::from_millis(40), // ca. 25 FPS
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -21,7 +28,24 @@ impl CustomVideoStream {
|
||||
impl Stream for CustomVideoStream {
|
||||
type Item = Result<Bytes, StreamError>;
|
||||
|
||||
fn poll_next(mut self: Pin<&mut Self>, _cx: &mut Context<'_>,) -> Poll<Option<Self::Item>> {
|
||||
Poll::Ready(Some(Ok(self.buffer.next_chunk())))
|
||||
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
||||
// is a timer active ?
|
||||
if let Some(ref mut delay) = self.delay {
|
||||
if delay.as_mut().poll(cx).is_pending() {
|
||||
return Poll::Pending;
|
||||
}
|
||||
}
|
||||
|
||||
let chunk = self.buffer.next_chunk();
|
||||
|
||||
// new sleep-timer
|
||||
self.delay = Some(Box::pin(sleep(self.frame_interval)));
|
||||
|
||||
// Pre-register waker so we get notified even if the consumer doesn't poll again immediately.
|
||||
if let Some(ref mut delay) = self.delay {
|
||||
let _ = delay.as_mut().poll(cx);
|
||||
}
|
||||
|
||||
Poll::Ready(Some(Ok(chunk)))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@ fn encode_pcr(pcr: u64) -> [u8; 6] {
|
||||
]
|
||||
}
|
||||
|
||||
type TsInfoExtraction = (Vec<(usize, Option<(usize, usize, u16)>)>, Vec<(u16, u8)>);
|
||||
type TsInfoExtraction = (Vec<(usize, Option<(usize, usize, u16)>)>, HashMap<u16, u8>);
|
||||
|
||||
/// Extracts PTS and DTS info from MPEG-TS data.
|
||||
/// Returns a vector of tuples containing:
|
||||
@@ -158,30 +158,27 @@ pub fn extract_pts_dts_indices_with_continuity(ts_data: &[u8]) -> TsInfoExtracti
|
||||
result[first_dts_idx] = (idx, Some((pts, dts, (avg_diff & 0xFFFF) as u16)));
|
||||
}
|
||||
}
|
||||
let mut vec = Vec::with_capacity(continuity_counters.len());
|
||||
vec.extend(continuity_counters.iter().map(|(&k, &v)| (k, v)));
|
||||
|
||||
(result, vec)
|
||||
}
|
||||
|
||||
/// Replace PTS and DTS timestamps in the TS packet slice
|
||||
fn replace_pts_dts(packet_slice: &[u8], pts_index: usize, dts_index: usize, new_presentation_ts: u64, new_decoding_ts: u64) -> Vec<u8> {
|
||||
let before_pts = &packet_slice[..pts_index];
|
||||
let between_pts_dts = &packet_slice[pts_index + 5..dts_index];
|
||||
let after_dts = &packet_slice[dts_index + 5..];
|
||||
|
||||
let new_presentation_ts_bytes = encode_timestamp(new_presentation_ts);
|
||||
let new_decoding_ts_bytes = encode_timestamp(new_decoding_ts);
|
||||
|
||||
let mut new_packet = Vec::with_capacity(packet_slice.len());
|
||||
new_packet.extend_from_slice(before_pts);
|
||||
new_packet.extend_from_slice(&new_presentation_ts_bytes);
|
||||
new_packet.extend_from_slice(between_pts_dts);
|
||||
new_packet.extend_from_slice(&new_decoding_ts_bytes);
|
||||
new_packet.extend_from_slice(after_dts);
|
||||
|
||||
new_packet
|
||||
(result, continuity_counters)
|
||||
}
|
||||
//
|
||||
// /// Replace PTS and DTS timestamps in the TS packet slice
|
||||
// fn replace_pts_dts(packet_slice: &[u8], pts_index: usize, dts_index: usize, new_presentation_ts: u64, new_decoding_ts: u64) -> Vec<u8> {
|
||||
// let before_pts = &packet_slice[..pts_index];
|
||||
// let between_pts_dts = &packet_slice[pts_index + 5..dts_index];
|
||||
// let after_dts = &packet_slice[dts_index + 5..];
|
||||
//
|
||||
// let new_presentation_ts_bytes = encode_timestamp(new_presentation_ts);
|
||||
// let new_decoding_ts_bytes = encode_timestamp(new_decoding_ts);
|
||||
//
|
||||
// let mut new_packet = Vec::with_capacity(packet_slice.len());
|
||||
// new_packet.extend_from_slice(before_pts);
|
||||
// new_packet.extend_from_slice(&new_presentation_ts_bytes);
|
||||
// new_packet.extend_from_slice(between_pts_dts);
|
||||
// new_packet.extend_from_slice(&new_decoding_ts_bytes);
|
||||
// new_packet.extend_from_slice(after_dts);
|
||||
//
|
||||
// new_packet
|
||||
// }
|
||||
|
||||
/// Finds TS alignment by checking for 0x47 sync byte every 188 bytes
|
||||
fn find_ts_alignment(buf: &[u8]) -> Option<usize> {
|
||||
@@ -238,8 +235,8 @@ pub struct TransportStreamBuffer {
|
||||
timestamp_offset: u64,
|
||||
length: usize,
|
||||
stream_duration_90khz: u64, // Duration in 90kHz units
|
||||
initial_continuity_counters: Arc<Vec<(u16,u8)>>,
|
||||
continuity_counters: Vec<(u16,u8)>,
|
||||
initial_continuity_counters: Arc<HashMap<u16,u8>>,
|
||||
continuity_counters: HashMap<u16,u8>,
|
||||
}
|
||||
|
||||
impl Clone for TransportStreamBuffer {
|
||||
@@ -289,77 +286,72 @@ impl TransportStreamBuffer {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns next chunks with adjusted PTS/DTS and PCR
|
||||
/// Returns next chunks with adjusted PTS/DTS and PCR
|
||||
pub fn next_chunk(&mut self) -> Bytes {
|
||||
let mut bytes = BytesMut::with_capacity(CHUNK_SIZE);
|
||||
// we send this amount of packets in one chunk
|
||||
|
||||
// Change continuity_counters to HashMap<u16, u8> instead of Vec<(u16,u8)>
|
||||
// to avoid O(n) searches per PID. (Change type in struct definition)
|
||||
// continuity_counters: HashMap<u16, u8>,
|
||||
|
||||
let mut packets_remaining = PACKET_COUNT;
|
||||
|
||||
while packets_remaining > 0 {
|
||||
if self.current_pos >= self.length {
|
||||
// Loop back and update timestamp offset
|
||||
self.current_pos = 0;
|
||||
|
||||
let new_offset = (self.timestamp_offset + self.stream_duration_90khz) % MAX_PTS_DTS;
|
||||
self.timestamp_offset = new_offset;
|
||||
|
||||
self.timestamp_offset = (self.timestamp_offset + self.stream_duration_90khz) % MAX_PTS_DTS;
|
||||
self.current_dts = 0;
|
||||
}
|
||||
|
||||
let current_pos = self.current_pos;
|
||||
let (packet_start, pts_dts_maybe) = self.packet_indices[current_pos];
|
||||
let (packet_start, pts_dts_maybe) = self.packet_indices[self.current_pos];
|
||||
let packet = &self.buffer[packet_start..packet_start + TS_PACKET_SIZE];
|
||||
|
||||
let mut new_packet = packet.to_vec();
|
||||
// Reuse stack array instead of heap allocation
|
||||
let mut new_packet = [0u8; TS_PACKET_SIZE];
|
||||
new_packet.copy_from_slice(packet);
|
||||
|
||||
// update continuity counter
|
||||
let pid = (u16::from(new_packet[1] & 0x1F) << 8) | u16::from(new_packet[2]);
|
||||
// let counter = &mut self.continuity_counters[pid as usize];
|
||||
// ---- Continuity Counter ----
|
||||
// let pid = (u16::from(new_packet[1] & 0x1F) << 8) | u16::from(new_packet[2]);
|
||||
// let counter = self.continuity_counters.entry(pid).or_insert(0);
|
||||
// new_packet[3] = (new_packet[3] & 0xF0) | (*counter & 0x0F);
|
||||
// *counter = (*counter + 1) % 16;
|
||||
|
||||
// Find the entry with this PID (mutable), or insert a new entry if it doesn't exist
|
||||
if let Some((_pid, counter)) = self.continuity_counters.iter_mut().find(|(p, _)| *p == pid) {
|
||||
new_packet[3] = (new_packet[3] & 0xF0) | (*counter & 0x0F);
|
||||
let pid = (u16::from(new_packet[1] & 0x1F) << 8) | u16::from(new_packet[2]);
|
||||
let afc = (new_packet[3] >> 4) & 0b11;
|
||||
let has_payload = (afc & 0x01) != 0;
|
||||
let counter = self.continuity_counters.entry(pid).or_insert(0);
|
||||
new_packet[3] = (new_packet[3] & 0xF0) | (*counter & 0x0F);
|
||||
if pid != 0x1FFF && has_payload {
|
||||
*counter = (*counter + 1) % 16;
|
||||
} else {
|
||||
// This should not happen because we scan the entire pids.
|
||||
// PID not present yet, insert new entry with counter = 0
|
||||
self.continuity_counters.push((pid, 1)); // Start at 1, since we immediately use the counter
|
||||
new_packet[3] &= 0xF0;
|
||||
}
|
||||
|
||||
// adjust PCR based on the original PCR, then add the offset
|
||||
// ---- PCR update ----
|
||||
let afc = (new_packet[3] >> 4) & 0b11;
|
||||
if afc == 2 || afc == 3 {
|
||||
let adaptation_len = new_packet[4] as usize;
|
||||
if adaptation_len > 0 {
|
||||
if adaptation_len >= 7 {
|
||||
let flags = new_packet[5];
|
||||
if (flags & ADAPTATION_FIELD_FLAG_PCR) != 0 {
|
||||
let pcr_pos = 6;
|
||||
if new_packet.len() >= pcr_pos + 6 {
|
||||
// read original PCR
|
||||
let orig_pcr = decode_pcr(&new_packet[pcr_pos..pcr_pos + 6]);
|
||||
// Apply PCR offset; PCR runs at 27 MHz, so multiply by 300 to convert from 90 kHz to 27 MHz
|
||||
let offset = self.timestamp_offset * 300;
|
||||
let new_pcr = (orig_pcr + offset) % MAX_PCR;
|
||||
let pcr_bytes = encode_pcr(new_pcr);
|
||||
new_packet[pcr_pos..pcr_pos + 6].copy_from_slice(&pcr_bytes);
|
||||
}
|
||||
let orig_pcr = decode_pcr(&new_packet[pcr_pos..pcr_pos + 6]);
|
||||
let new_pcr = (orig_pcr + self.timestamp_offset * 300) % MAX_PCR;
|
||||
let pcr_bytes = encode_pcr(new_pcr);
|
||||
new_packet[pcr_pos..pcr_pos + 6].copy_from_slice(&pcr_bytes);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// adjust PTS/DTS
|
||||
// ---- PTS/DTS update ----
|
||||
if let Some((pts_offset, dts_offset, _diff)) = pts_dts_maybe {
|
||||
let orig_dts = decode_timestamp(&new_packet[dts_offset..dts_offset + 5]);
|
||||
let new_decoding_ts = (orig_dts + self.timestamp_offset) % MAX_PTS_DTS;
|
||||
let decoding_ts_orig = decode_timestamp(&new_packet[dts_offset..dts_offset + 5]);
|
||||
let presentation_ts_orig = decode_timestamp(&new_packet[pts_offset..pts_offset + 5]);
|
||||
let new_decoding_ts = (decoding_ts_orig + self.timestamp_offset) % MAX_PTS_DTS;
|
||||
let new_presentation_ts = (presentation_ts_orig + self.timestamp_offset) % MAX_PTS_DTS;
|
||||
|
||||
let orig_presentation_ts = decode_timestamp(&new_packet[pts_offset..pts_offset + 5]);
|
||||
let new_presentation_ts = (orig_presentation_ts + self.timestamp_offset) % MAX_PTS_DTS;
|
||||
|
||||
let replaced = replace_pts_dts(&new_packet, pts_offset, dts_offset, new_presentation_ts, new_decoding_ts);
|
||||
new_packet = replaced;
|
||||
// Replace in-place, no new Vec allocation
|
||||
new_packet[pts_offset..pts_offset + 5].copy_from_slice(&encode_timestamp(new_decoding_ts));
|
||||
new_packet[dts_offset..dts_offset + 5].copy_from_slice(&encode_timestamp(new_presentation_ts));
|
||||
}
|
||||
|
||||
bytes.extend_from_slice(&new_packet);
|
||||
|
||||
@@ -71,7 +71,7 @@
|
||||
--text-color: #ffffff;
|
||||
--modest-text-color: #a7abb4;
|
||||
--background-color: #0a0b10;
|
||||
--border-color: rgba(90, 140, 255, 0.25);
|
||||
--border-color: rgba(60, 100, 195, 0.25);
|
||||
--input-background-color: rgba(255, 255, 255, 0);
|
||||
--input-border-color: var(--border-color);
|
||||
--input-focus-border-color: rgba(80, 150, 255, 0.8);
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
padding-left: var(--padding-mini);
|
||||
padding-right: var(--padding-mini);
|
||||
border: 1px solid var(--input-border-color);
|
||||
border-radius: var(--border-radius);
|
||||
background-color: var(--input-background-color);
|
||||
|
||||
@@ -37,7 +37,7 @@ pub fn UserEdit() -> Html {
|
||||
}
|
||||
});
|
||||
|
||||
let handle_back = {
|
||||
let handle_cancel = {
|
||||
let userlist_ctx = userlist_ctx.clone();
|
||||
Callback::from(move |_| {
|
||||
userlist_ctx.active_page.set(UserlistPage::List);
|
||||
@@ -45,6 +45,13 @@ pub fn UserEdit() -> Html {
|
||||
})
|
||||
};
|
||||
|
||||
let handle_back = {
|
||||
let handle_cancel = handle_cancel.clone();
|
||||
Callback::from(move |_| {
|
||||
handle_cancel.emit(());
|
||||
})
|
||||
};
|
||||
|
||||
let handle_user_save = {
|
||||
let userlist = userlist_ctx.clone();
|
||||
let handleback = handle_back.clone();
|
||||
@@ -56,16 +63,22 @@ pub fn UserEdit() -> Html {
|
||||
let userlist = userlist.clone();
|
||||
let translate = translate.clone();
|
||||
spawn_local(async move {
|
||||
match if is_update { services.user.update_user(target.clone(), user.clone()).await } else { services.user.create_user(target.clone(), user.clone()).await } {
|
||||
let save_result = if is_update {
|
||||
services.user.update_user(target.clone(), user.clone()).await
|
||||
} else {
|
||||
services.user.create_user(target.clone(), user.clone()).await
|
||||
};
|
||||
match save_result {
|
||||
Ok(()) => {
|
||||
let new_user = Rc::new(TargetUser {target: target.clone(), credentials: Rc::new(user.clone()) });
|
||||
let new_user_list = if let Some(user_list) = userlist.users.as_ref() {
|
||||
let mut new_list: Vec<Rc<TargetUser>> = user_list.iter().map(|target_user| {
|
||||
let mut cloned = target_user.as_ref().clone();
|
||||
if is_update && cloned.target == target && cloned.credentials.username == user.username {
|
||||
cloned.credentials = Rc::new(user.clone());
|
||||
}
|
||||
Rc::new(cloned)
|
||||
let mut new_list: Vec<Rc<TargetUser>> = user_list.iter().map(|target_user| {
|
||||
let mut cloned_target = target_user.as_ref().clone();
|
||||
if is_update && cloned_target.credentials.username == user.username {
|
||||
cloned_target.credentials = Rc::new(user.clone());
|
||||
cloned_target.target = target.clone();
|
||||
}
|
||||
Rc::new(cloned_target)
|
||||
}).collect();
|
||||
|
||||
if !is_update {
|
||||
@@ -98,7 +111,7 @@ pub fn UserEdit() -> Html {
|
||||
</div>
|
||||
<div class="tp__userlist-edit__body tp__list-create__body">
|
||||
<Card>
|
||||
<ProxyUserCredentialsForm server={server.clone()} targets={targets.clone()} user={(*userlist_ctx.selected_user).clone()} on_save={handle_user_save}/>
|
||||
<ProxyUserCredentialsForm server={server.clone()} targets={targets.clone()} user={(*userlist_ctx.selected_user).clone()} on_save={handle_user_save} on_cancel={handle_cancel}/>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use std::rc::Rc;
|
||||
use chrono::{Duration, Utc};
|
||||
use log::debug;
|
||||
use yew::prelude::*;
|
||||
use yew_i18n::use_translation;
|
||||
use shared::model::{ApiProxyServerInfoDto, ConfigTargetDto, ProxyType, ProxyUserCredentialsDto, ProxyUserStatus};
|
||||
@@ -39,6 +40,7 @@ pub struct ProxyUserCredentialsFormProps {
|
||||
pub targets: Rc<Vec<Rc<ConfigTargetDto>>>,
|
||||
pub server: Rc<Vec<ApiProxyServerInfoDto>>,
|
||||
pub on_save: Callback<(bool, String, ProxyUserCredentialsDto)>,
|
||||
pub on_cancel: Callback<()>,
|
||||
}
|
||||
|
||||
#[function_component]
|
||||
@@ -116,6 +118,13 @@ pub fn ProxyUserCredentialsForm(props: &ProxyUserCredentialsFormProps) -> Html {
|
||||
);
|
||||
}
|
||||
|
||||
let handle_cancel = {
|
||||
let oncancel = props.on_cancel.clone();
|
||||
Callback::from(move |_| {
|
||||
oncancel.emit(());
|
||||
})
|
||||
};
|
||||
|
||||
let handle_save_user = {
|
||||
let user = form_state.clone();
|
||||
let original = props.user.clone();
|
||||
@@ -125,8 +134,11 @@ pub fn ProxyUserCredentialsForm(props: &ProxyUserCredentialsFormProps) -> Html {
|
||||
let onsave = props.on_save.clone();
|
||||
let is_update = update.clone();
|
||||
Callback::from(move |_| {
|
||||
let nothing_to_save = || services.toastr.warning(translate_clone.t("MESSAGES.SAVE.USER.NOTHING_TO_SAVE"));
|
||||
if let Some(target_name) = (*target).as_ref().cloned() {
|
||||
if user.modified() {
|
||||
let original_target = original.as_ref().map(|u| u.target.clone()).unwrap_or_default();
|
||||
let target_changed = target_name != original_target;
|
||||
if target_changed || user.modified() {
|
||||
let user = user.data();
|
||||
if let Err(err) = user.validate() {
|
||||
services.toastr.error(err.to_string());
|
||||
@@ -134,14 +146,16 @@ pub fn ProxyUserCredentialsForm(props: &ProxyUserCredentialsFormProps) -> Html {
|
||||
match original.as_ref().map(|t| t.credentials.clone()) {
|
||||
None => onsave.emit((*is_update, target_name, user.clone())),
|
||||
Some(original_user) => {
|
||||
if &(*original_user) != user {
|
||||
if target_changed || &(*original_user) != user {
|
||||
onsave.emit((*is_update, target_name, user.clone()));
|
||||
} else {
|
||||
nothing_to_save();
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
} else {
|
||||
services.toastr.warning(translate_clone.t("MESSAGES.SAVE.USER.NOTHING_TO_SAVE"));
|
||||
nothing_to_save();
|
||||
}
|
||||
} else {
|
||||
services.toastr.error(translate_clone.t("MESSAGES.SAVE.USER.TARGET_NOT_SELECTED"));
|
||||
@@ -227,6 +241,10 @@ pub fn ProxyUserCredentialsForm(props: &ProxyUserCredentialsFormProps) -> Html {
|
||||
icon="Save"
|
||||
title={ translate.t("LABEL.SAVE")}
|
||||
onclick={handle_save_user}></TextButton>
|
||||
<TextButton class="secondary" name="cancel_user"
|
||||
icon="Cancel"
|
||||
title={ translate.t("LABEL.CANCEL")}
|
||||
onclick={handle_cancel}></TextButton>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -99,6 +99,13 @@ pub fn truncate_string(s: &str, max_len: usize) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn mask_credentials(s: &str) -> String {
|
||||
match s.chars().next() {
|
||||
Some(first) => format!("{}...", first),
|
||||
None => "...".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use std::collections::HashSet;
|
||||
|
||||
Reference in New Issue
Block a user