Added kick duration for web_ui kick

This commit is contained in:
euzu
2025-12-12 21:16:52 +01:00
parent b951794bc4
commit efc23c2b21
14 changed files with 28 additions and 26 deletions
+1 -2
View File
@@ -35,8 +35,7 @@
- Added extended debug logging for client requests and ID chain (request/action/virtual) to trace stream resolution.
- Fixed xtream series/catchup lookups using the series-info virtual_id so episode requests now keep their own virtual_id/session.
- Made cache storage more robust. Incomplete downloads will be deleted from cache.
- `kick_secs` added to config.yaml `web_ui` config. Default 30 seconds, if a user is kicked from the webui, he can't connect for this duration.
- `kick_secs` added to config.yaml `web_ui` config. Default 30 seconds, if a user is kicked from the webui, they can't connect for this duration.
# 3.2.0 (2025-11-14)
- Added `name` attribute to Staged Input.
+1 -1
View File
@@ -417,7 +417,7 @@ log:
- `content_security_policy`: configure Content-Security-Policy headers. When `enabled` is true, the default directives `default-src 'self'`, `script-src 'self' 'wasm-unsafe-eval' 'nonce-{nonce_b64}'`, and `frame-ancestors 'none'` are applied. Additional directives can be added via `custom-attributes`. Enabling CSP may block external images/logos unless allowed via directives like `img-src`.
- `path` is for web_ui path like `/ui` for reverse proxy integration if necessary.
- `player_server` optional, if set the server setting is used for the web-ui-player.
- `kick_secs` default 30 seconds, if a user is kicked from the webui, he can't connect for this duration.
- `kick_secs` default 30 seconds, if a user is kicked from the webui, they can't connect for this duration.
- `auth` for authentication settings
- `enabled` can be deactivated if `enabled` is set to `false`. If not set default is `true`.
- `issuer`
+1 -1
View File
@@ -164,7 +164,7 @@ pub(in crate::api) async fn handle_hls_stream_request(
CustomVideoStreamType::ChannelUnavailable);
let playlist = PLAYLIST_TEMPLATE.replace("{url}", &url);
hls_response(playlist.clone()).into_response()
hls_response(playlist).into_response()
} else {
axum::http::StatusCode::NOT_FOUND.into_response()
}
+1 -1
View File
@@ -119,7 +119,7 @@ async fn m3u_api_stream(
);
let virtual_id = pli.virtual_id;
if app_state.active_users.is_user_blocked_for_stream(stream_req.username, virtual_id).await {
if app_state.active_users.is_user_blocked_for_stream(&user.username, virtual_id).await {
return axum::http::StatusCode::BAD_REQUEST.into_response();
}
+1 -1
View File
@@ -255,7 +255,7 @@ async fn xtream_player_api_stream(
format!("Failed to read xtream item for stream id {req_virtual_id}")
);
let virtual_id = pli.virtual_id;
if app_state.active_users.is_user_blocked_for_stream(stream_req.username, virtual_id).await {
if app_state.active_users.is_user_blocked_for_stream(&user.username, virtual_id).await {
return axum::http::StatusCode::BAD_REQUEST.into_response();
}
+1 -1
View File
@@ -507,7 +507,7 @@ impl ActiveUserManager {
let now = current_time_secs();
connections.kicked.retain(|_, (expires_at, _)| *expires_at > now);
if let Some(username) = connections.key_by_addr.get(addr).cloned() {
let expires_at = now + blocked_secs;
let expires_at = now + blocked_secs.clamp(1,86_400); // max 1 day
connections.kicked.insert(username, (expires_at, virtual_id));
}
}
@@ -24,6 +24,7 @@
flex: 1 1 auto;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
span {
pointer-events: none;
@@ -59,7 +59,8 @@
&-input {
height: 20rem;
width: 100rem;
width: 100%;
max-width: 100%;
}
}
@@ -6,12 +6,13 @@
.tp__input-wrapper {
height: 2.5rem;
padding: 0 var(--padding-mini);
display: flex;
align-items: center;
justify-content: space-between;
}
&:hover {
fill: var(--text-color);
cursor: pointer;
filter: brightness(1.2);
}
@@ -8,7 +8,6 @@
&:hover {
fill: var(--text-color);
cursor: pointer;
filter: brightness(1.2);
}
@@ -24,7 +24,12 @@ pub struct ClusterFlagsInputProps {
pub fn ClusterFlagsInput(props: &ClusterFlagsInputProps) -> Html {
let translate = use_translation();
let flags = use_state(|| props.value.unwrap_or_else(ClusterFlags::all));
let flags = use_state(|| {
props.value.unwrap_or_else(|| match props.mode {
ClusterFlagsInputMode::NoneIsAll => ClusterFlags::all(),
ClusterFlagsInputMode::NoneIsNone => ClusterFlags::empty(),
})
});
{
let set_flags = flags.clone();
use_effect_with((props.value, props.mode), move |(val, cmode)| {
@@ -1,4 +1,3 @@
use log::debug;
use yew::prelude::*;
use web_sys::MouseEvent;
@@ -28,8 +27,6 @@ pub fn CustomDialog(props: &CustomDialogProps) -> Html {
});
}
debug!(" props close {}", props.close_on_backdrop_click);
// Handle backdrop click
let on_backdrop_click = {
let on_close = props.on_close.clone();
@@ -25,14 +25,6 @@ pub fn FilterEditor(props: &FilterEditorProps) -> Html {
let parsed_filter_state = use_state(|| None);
let valid_filter_state = use_state(|| true);
{
let templates = templates_state.clone();
let cfg_templates = config_ctx.config.as_ref().and_then(|c| c.sources.templates.clone());
use_effect_with(cfg_templates, move |templ| {
templates.set(templ.clone());
});
}
{
let templates = templates_state.clone();
let cfg_templates = config_ctx.config.as_ref().and_then(|c| c.sources.templates.clone());
@@ -75,13 +67,16 @@ pub fn FilterEditor(props: &FilterEditorProps) -> Html {
let handle_filter_input = {
let filter = filter_state.clone();
let on_filter_change = props.on_filter_change.clone();
Callback::from(move |event: InputEvent| {
if let Some(input) = event.target_dyn_into::<web_sys::HtmlTextAreaElement>() {
let value = input.value();
if value.is_empty() {
filter.set(None);
on_filter_change.emit(None);
} else {
filter.set(Some(value));
filter.set(Some(value.clone()));
on_filter_change.emit(Some(value));
}
}
})
@@ -79,20 +79,24 @@ pub fn FilterInput(props: &FilterInputProps) -> Html {
let dialog = dialog.clone();
let current_filter = filter_state.clone();
let handle_filter_edit = handle_filter_edit.clone();
let handle_templates_edit =handle_templates_edit.clone();
let handle_templates_edit = handle_templates_edit.clone();
let dialog_actions = dialog_actions.clone();
Callback::from(move |e: MouseEvent| {
e.prevent_default();
let current_filter = (*current_filter).clone();
let handle_filter_edit = handle_filter_edit.clone();
let handle_templates_edit =handle_templates_edit.clone();
let handle_templates_edit = handle_templates_edit.clone();
let actions = dialog_actions.clone();
let dlg = dialog.clone();
spawn_local(async move {
let filter_view = html!{<FilterEditor filter={current_filter}
on_filter_change={handle_filter_edit}
on_templates_change={handle_templates_edit} />};
let _result = dlg.content(filter_view, (*actions).clone(), false).await;
let result = dlg.content(filter_view, (*actions).clone(), false).await;
if result == DialogResult::Ok {
// Propagate the updated filter to parent via on_change
// Note: Need access to filter_state's current value and props.on_change here
}
});
})
};