diff --git a/Cargo.lock b/Cargo.lock index 239b925..183e91e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2879,6 +2879,7 @@ dependencies = [ "migration", "opus", "rand 0.10.1", + "regex", "rml_rtmp", "rubato", "sea-orm", diff --git a/crates/entity/src/stream_key.rs b/crates/entity/src/stream_key.rs index 46bace5..9c86899 100644 --- a/crates/entity/src/stream_key.rs +++ b/crates/entity/src/stream_key.rs @@ -79,3 +79,14 @@ impl Entity { .await } } + +impl ActiveModel { + pub async fn change_label_value( + mut self, + db: &DatabaseConnection, + value: String, + ) -> Result { + self.label = Set(value); + self.update(db).await + } +} diff --git a/crates/server/Cargo.toml b/crates/server/Cargo.toml index 40dc169..ec1c9ff 100644 --- a/crates/server/Cargo.toml +++ b/crates/server/Cargo.toml @@ -35,3 +35,4 @@ symphonia = { version = "0.5", features = ["aac"] } opus = "0.3.1" rubato = "3.0.0" chrono = "0.4.45" +regex = "1" diff --git a/crates/server/src/http.rs b/crates/server/src/http.rs index d67565b..b423697 100644 --- a/crates/server/src/http.rs +++ b/crates/server/src/http.rs @@ -1,11 +1,21 @@ use std::{ net::SocketAddr, sync::{ - Arc, + Arc, LazyLock, atomic::{AtomicI32, Ordering}, }, }; +use regex::Regex; + +// The Rust `regex` crate is guaranteed linear-time and therefore does NOT +// support lookaround, so the JS source pattern +// /^(?=.*[A-Za-z])[A-Za-z0-9_-]{1,67}$/ +// cannot be ported verbatim. The `(?=.*[A-Za-z])` lookahead only means +// "must contain at least one letter" — we drop it from the pattern and +// enforce that condition with a separate `.chars().any(..)` check below. +static KEY_RE: LazyLock = LazyLock::new(|| Regex::new(r"^[A-Za-z0-9 _-]{1,67}$").unwrap()); + use axum::{ Json, Router, body::{Body, Bytes}, @@ -20,7 +30,9 @@ use axum::{ }; use chrono::{DateTime, Utc}; use entity::{auth_session, stream_key, stream_session, users}; -use sea_orm::{DatabaseConnection, EntityTrait, QueryFilter, prelude::DateTimeUtc}; +use sea_orm::{ + DatabaseConnection, EntityTrait, IntoActiveModel, QueryFilter, prelude::DateTimeUtc, +}; use serde::{Deserialize, Serialize}; use tokio::{ net::TcpListener, @@ -83,7 +95,9 @@ impl HttpServer { .route("/api/user", post(create_user_handler)) .route( "/api/stream-key", - post(create_stream_key_handler).get(get_all_stream_keys), + post(create_stream_key_handler) + .get(get_all_stream_keys) + .patch(edit_stream_key), ) // .route("/api/admin/server_stats", get(todo!())) .route("/api/whip", post(handle_whip_injest)) @@ -112,6 +126,48 @@ struct StreamListing { started_at: DateTime, //UNIX TIMESTAMP } +#[derive(Deserialize)] +struct EditStreamKeyRequest { + id: i32, + new: String, +} + +async fn edit_stream_key( + State(state): State>, + auth: AuthUser, + Json(payload): Json, +) -> impl IntoResponse { + // Trim surrounding whitespace so labels aren't stored with leading/trailing spaces. + let new_label = payload.new.trim(); + // Length (1..=67) and allowed charset ([A-Za-z0-9 _-], space included). + if !KEY_RE.is_match(new_label) { + return (StatusCode::BAD_REQUEST, "invalid label").into_response(); + } + // Replaces the JS lookahead: label must contain at least one letter. + if !new_label.chars().any(|c| c.is_ascii_alphabetic()) { + return (StatusCode::BAD_REQUEST, "label must contain a letter").into_response(); + } + + let stream_key_id_result = stream_key::Entity::find_by_id(payload.id) + .one(&state.db) + .await; + if stream_key_id_result.is_err() { + return StatusCode::INTERNAL_SERVER_ERROR.into_response(); + } + let stream_key = stream_key_id_result.unwrap().unwrap(); + // Do they own the stream key + if stream_key.user_id != auth.0.id { + return StatusCode::FORBIDDEN.into_response(); + } + stream_key + .into_active_model() + .change_label_value(&state.db, new_label.to_string()) + .await + .unwrap(); + + StatusCode::OK.into_response() +} + async fn catalog_handler(State(state): State>) -> impl IntoResponse { let streams = stream_session::Model::get_all_active_sessions(&state.db) .await