Valider fase 15–16: sikkerhet, konsistens og atomisk toggle
Tre fikser funnet under validering: 1. SIKKERHET: Admin-endepunkter manglet autorisasjonssjekk. Alle /admin/*-endepunkter brukte kun AuthUser (autentisert), ikke admin-rolle. Ny AdminUser-extractor sjekker owner/admin-edge til samling — returnerer 403 Forbidden for ikke-admins. Berører: maintenance, jobs, resources, health, ai, usage. 2. Race condition i toggle_effect: les-modifiser-skriv uten transaksjon på active_effects JSON. Erstattet med atomisk PG jsonb-operasjon. 3. Manglende updated_by i set_gain, set_mute, set_mixer_role, toggle_effect. Nå spores hvem som endret mixer-tilstanden.
This commit is contained in:
parent
b7e7fbf45b
commit
1e34c3c67a
6 changed files with 125 additions and 69 deletions
|
|
@ -11,7 +11,7 @@ use serde::{Deserialize, Serialize};
|
|||
use sqlx::PgPool;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::auth::AuthUser;
|
||||
use crate::auth::AdminUser;
|
||||
use crate::AppState;
|
||||
|
||||
// =============================================================================
|
||||
|
|
@ -92,7 +92,7 @@ fn internal_error(msg: &str) -> (StatusCode, Json<ErrorResponse>) {
|
|||
|
||||
pub async fn ai_overview(
|
||||
State(state): State<AppState>,
|
||||
_user: AuthUser,
|
||||
_admin: AdminUser,
|
||||
) -> Result<Json<AiOverviewResponse>, (StatusCode, Json<ErrorResponse>)> {
|
||||
let aliases = sqlx::query_as::<_, AiModelAlias>(
|
||||
"SELECT id, alias, description, is_active, created_at FROM ai_model_aliases ORDER BY alias"
|
||||
|
|
@ -178,7 +178,7 @@ pub struct UpdateAliasRequest {
|
|||
|
||||
pub async fn update_alias(
|
||||
State(state): State<AppState>,
|
||||
_user: AuthUser,
|
||||
_admin: AdminUser,
|
||||
Json(req): Json<UpdateAliasRequest>,
|
||||
) -> Result<Json<serde_json::Value>, (StatusCode, Json<ErrorResponse>)> {
|
||||
let rows = sqlx::query(
|
||||
|
|
@ -195,7 +195,7 @@ pub async fn update_alias(
|
|||
return Err(bad_request("Alias ikke funnet"));
|
||||
}
|
||||
|
||||
tracing::info!(alias_id = %req.id, user = %_user.node_id, "Admin: alias oppdatert");
|
||||
tracing::info!(alias_id = %req.id, user = %_admin.node_id, "Admin: alias oppdatert");
|
||||
Ok(Json(serde_json::json!({ "success": true })))
|
||||
}
|
||||
|
||||
|
|
@ -211,7 +211,7 @@ pub struct CreateAliasRequest {
|
|||
|
||||
pub async fn create_alias(
|
||||
State(state): State<AppState>,
|
||||
_user: AuthUser,
|
||||
_admin: AdminUser,
|
||||
Json(req): Json<CreateAliasRequest>,
|
||||
) -> Result<Json<serde_json::Value>, (StatusCode, Json<ErrorResponse>)> {
|
||||
if req.alias.trim().is_empty() {
|
||||
|
|
@ -233,7 +233,7 @@ pub async fn create_alias(
|
|||
}
|
||||
})?;
|
||||
|
||||
tracing::info!(alias = %req.alias, user = %_user.node_id, "Admin: alias opprettet");
|
||||
tracing::info!(alias = %req.alias, user = %_admin.node_id, "Admin: alias opprettet");
|
||||
Ok(Json(serde_json::json!({ "id": id, "success": true })))
|
||||
}
|
||||
|
||||
|
|
@ -250,7 +250,7 @@ pub struct UpdateProviderRequest {
|
|||
|
||||
pub async fn update_provider(
|
||||
State(state): State<AppState>,
|
||||
_user: AuthUser,
|
||||
_admin: AdminUser,
|
||||
Json(req): Json<UpdateProviderRequest>,
|
||||
) -> Result<Json<serde_json::Value>, (StatusCode, Json<ErrorResponse>)> {
|
||||
if let Some(priority) = req.priority {
|
||||
|
|
@ -270,7 +270,7 @@ pub async fn update_provider(
|
|||
.map_err(|e| internal_error(&format!("Feil ved oppdatering av provider status: {e}")))?;
|
||||
}
|
||||
|
||||
tracing::info!(provider_id = %req.id, user = %_user.node_id, "Admin: provider oppdatert");
|
||||
tracing::info!(provider_id = %req.id, user = %_admin.node_id, "Admin: provider oppdatert");
|
||||
Ok(Json(serde_json::json!({ "success": true })))
|
||||
}
|
||||
|
||||
|
|
@ -289,7 +289,7 @@ pub struct CreateProviderRequest {
|
|||
|
||||
pub async fn create_provider(
|
||||
State(state): State<AppState>,
|
||||
_user: AuthUser,
|
||||
_admin: AdminUser,
|
||||
Json(req): Json<CreateProviderRequest>,
|
||||
) -> Result<Json<serde_json::Value>, (StatusCode, Json<ErrorResponse>)> {
|
||||
if req.model.trim().is_empty() {
|
||||
|
|
@ -317,7 +317,7 @@ pub async fn create_provider(
|
|||
}
|
||||
})?;
|
||||
|
||||
tracing::info!(model = %req.model, alias_id = %req.alias_id, user = %_user.node_id, "Admin: provider opprettet");
|
||||
tracing::info!(model = %req.model, alias_id = %req.alias_id, user = %_admin.node_id, "Admin: provider opprettet");
|
||||
Ok(Json(serde_json::json!({ "id": id, "success": true })))
|
||||
}
|
||||
|
||||
|
|
@ -332,7 +332,7 @@ pub struct DeleteProviderRequest {
|
|||
|
||||
pub async fn delete_provider(
|
||||
State(state): State<AppState>,
|
||||
_user: AuthUser,
|
||||
_admin: AdminUser,
|
||||
Json(req): Json<DeleteProviderRequest>,
|
||||
) -> Result<Json<serde_json::Value>, (StatusCode, Json<ErrorResponse>)> {
|
||||
let rows = sqlx::query("DELETE FROM ai_model_providers WHERE id = $1")
|
||||
|
|
@ -345,7 +345,7 @@ pub async fn delete_provider(
|
|||
return Err(bad_request("Provider ikke funnet"));
|
||||
}
|
||||
|
||||
tracing::info!(provider_id = %req.id, user = %_user.node_id, "Admin: provider slettet");
|
||||
tracing::info!(provider_id = %req.id, user = %_admin.node_id, "Admin: provider slettet");
|
||||
Ok(Json(serde_json::json!({ "success": true })))
|
||||
}
|
||||
|
||||
|
|
@ -362,7 +362,7 @@ pub struct UpdateRoutingRequest {
|
|||
|
||||
pub async fn update_routing(
|
||||
State(state): State<AppState>,
|
||||
_user: AuthUser,
|
||||
_admin: AdminUser,
|
||||
Json(req): Json<UpdateRoutingRequest>,
|
||||
) -> Result<Json<serde_json::Value>, (StatusCode, Json<ErrorResponse>)> {
|
||||
if req.job_type.trim().is_empty() || req.alias.trim().is_empty() {
|
||||
|
|
@ -381,7 +381,7 @@ pub async fn update_routing(
|
|||
.await
|
||||
.map_err(|e| internal_error(&format!("Feil ved oppdatering av ruting: {e}")))?;
|
||||
|
||||
tracing::info!(job_type = %req.job_type, alias = %req.alias, user = %_user.node_id, "Admin: ruting oppdatert");
|
||||
tracing::info!(job_type = %req.job_type, alias = %req.alias, user = %_admin.node_id, "Admin: ruting oppdatert");
|
||||
Ok(Json(serde_json::json!({ "success": true })))
|
||||
}
|
||||
|
||||
|
|
@ -396,7 +396,7 @@ pub struct DeleteRoutingRequest {
|
|||
|
||||
pub async fn delete_routing(
|
||||
State(state): State<AppState>,
|
||||
_user: AuthUser,
|
||||
_admin: AdminUser,
|
||||
Json(req): Json<DeleteRoutingRequest>,
|
||||
) -> Result<Json<serde_json::Value>, (StatusCode, Json<ErrorResponse>)> {
|
||||
let rows = sqlx::query("DELETE FROM ai_job_routing WHERE job_type = $1")
|
||||
|
|
@ -409,7 +409,7 @@ pub async fn delete_routing(
|
|||
return Err(bad_request("Rutingregel ikke funnet"));
|
||||
}
|
||||
|
||||
tracing::info!(job_type = %req.job_type, user = %_user.node_id, "Admin: ruting slettet");
|
||||
tracing::info!(job_type = %req.job_type, user = %_admin.node_id, "Admin: ruting slettet");
|
||||
Ok(Json(serde_json::json!({ "success": true })))
|
||||
}
|
||||
|
||||
|
|
@ -425,7 +425,7 @@ pub struct UsageQueryParams {
|
|||
|
||||
pub async fn ai_usage(
|
||||
State(state): State<AppState>,
|
||||
_user: AuthUser,
|
||||
_admin: AdminUser,
|
||||
axum::extract::Query(params): axum::extract::Query<UsageQueryParams>,
|
||||
) -> Result<Json<Vec<AiUsageSummary>>, (StatusCode, Json<ErrorResponse>)> {
|
||||
let days = params.days.unwrap_or(30).min(365);
|
||||
|
|
|
|||
|
|
@ -115,6 +115,9 @@ impl IntoResponse for AuthErrorKind {
|
|||
AuthErrorKind::UnknownIdentity => {
|
||||
(StatusCode::UNAUTHORIZED, "Ukjent brukeridentitet")
|
||||
}
|
||||
AuthErrorKind::Forbidden => {
|
||||
(StatusCode::FORBIDDEN, "Krever admin-tilgang")
|
||||
}
|
||||
AuthErrorKind::Internal => {
|
||||
(StatusCode::INTERNAL_SERVER_ERROR, "Intern feil ved autentisering")
|
||||
}
|
||||
|
|
@ -133,6 +136,7 @@ pub enum AuthErrorKind {
|
|||
MissingToken,
|
||||
InvalidToken(String),
|
||||
UnknownIdentity,
|
||||
Forbidden,
|
||||
Internal,
|
||||
}
|
||||
|
||||
|
|
@ -210,3 +214,62 @@ where
|
|||
|
||||
// We need FromRef to extract AppState from the state
|
||||
use axum::extract::FromRef;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// AdminUser extractor — krever owner/admin-edge til en samling
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Admin-bruker: autentisert bruker som har `owner`- eller `admin`-edge til
|
||||
/// minst én samling. Brukes som extractor på /admin/*-endepunkter.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AdminUser {
|
||||
pub node_id: Uuid,
|
||||
pub authentik_sub: String,
|
||||
}
|
||||
|
||||
impl<S> FromRequestParts<S> for AdminUser
|
||||
where
|
||||
S: Send + Sync,
|
||||
AppState: FromRef<S>,
|
||||
{
|
||||
type Rejection = AuthErrorKind;
|
||||
|
||||
async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
|
||||
// Autentiser først
|
||||
let user = AuthUser::from_request_parts(parts, state).await?;
|
||||
|
||||
let app_state = AppState::from_ref(state);
|
||||
|
||||
// Sjekk at brukeren er admin (owner/admin-edge til en samling)
|
||||
let is_admin = sqlx::query_scalar::<_, bool>(
|
||||
r#"
|
||||
SELECT EXISTS(
|
||||
SELECT 1 FROM edges
|
||||
WHERE source_id = $1
|
||||
AND edge_type IN ('owner', 'admin')
|
||||
AND target_id IN (SELECT id FROM nodes WHERE node_kind = 'collection')
|
||||
)
|
||||
"#,
|
||||
)
|
||||
.bind(user.node_id)
|
||||
.fetch_one(&app_state.db)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
tracing::error!(error = %e, "PG-feil ved admin-sjekk");
|
||||
AuthErrorKind::Internal
|
||||
})?;
|
||||
|
||||
if !is_admin {
|
||||
tracing::warn!(
|
||||
user_id = %user.node_id,
|
||||
"Ikke-admin forsøkte å bruke admin-endepunkt"
|
||||
);
|
||||
return Err(AuthErrorKind::Forbidden);
|
||||
}
|
||||
|
||||
Ok(AdminUser {
|
||||
node_id: user.node_id,
|
||||
authentik_sub: user.authentik_sub,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ use axum::{extract::{Query, State}, http::StatusCode, Json};
|
|||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::PgPool;
|
||||
|
||||
use crate::auth::AuthUser;
|
||||
use crate::auth::AdminUser;
|
||||
use crate::AppState;
|
||||
|
||||
// =============================================================================
|
||||
|
|
@ -436,7 +436,7 @@ fn read_service_logs(service: &str, max_lines: usize) -> Vec<LogEntry> {
|
|||
/// GET /admin/health — komplett serverhelse-dashboard.
|
||||
pub async fn health_dashboard(
|
||||
State(state): State<AppState>,
|
||||
_user: AuthUser,
|
||||
_admin: AdminUser,
|
||||
) -> Result<Json<HealthDashboard>, (StatusCode, Json<crate::intentions::ErrorResponse>)> {
|
||||
// Kjør alle tjeneste-sjekker parallelt
|
||||
let (pg, caddy, authentik, litellm, whisper, livekit) = tokio::join!(
|
||||
|
|
@ -463,7 +463,7 @@ pub async fn health_dashboard(
|
|||
|
||||
/// GET /admin/health/logs?service=maskinrommet&lines=50
|
||||
pub async fn health_logs(
|
||||
_user: AuthUser,
|
||||
_admin: AdminUser,
|
||||
Query(params): Query<LogsQuery>,
|
||||
) -> Json<LogsResponse> {
|
||||
let max_lines = params.lines.unwrap_or(50).min(200);
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ use serde::{Deserialize, Serialize};
|
|||
use sqlx::PgPool;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::auth::AuthUser;
|
||||
use crate::auth::{AdminUser, AuthUser};
|
||||
use crate::livekit;
|
||||
use crate::AppState;
|
||||
|
||||
|
|
@ -3927,7 +3927,7 @@ pub struct InitiateMaintenanceResponse {
|
|||
/// Kall GET /admin/maintenance_status først for å se aktive sesjoner.
|
||||
pub async fn initiate_maintenance(
|
||||
State(state): State<AppState>,
|
||||
_user: AuthUser,
|
||||
admin: AdminUser,
|
||||
Json(req): Json<InitiateMaintenanceRequest>,
|
||||
) -> Result<Json<InitiateMaintenanceResponse>, (StatusCode, Json<ErrorResponse>)> {
|
||||
let scheduled_at = chrono::DateTime::parse_from_rfc3339(&req.scheduled_at)
|
||||
|
|
@ -3938,7 +3938,7 @@ pub async fn initiate_maintenance(
|
|||
return Err(bad_request("scheduled_at kan ikke være i fortiden"));
|
||||
}
|
||||
|
||||
let user_id = _user.node_id;
|
||||
let user_id = admin.node_id;
|
||||
|
||||
let announcement_id = state
|
||||
.maintenance
|
||||
|
|
@ -3963,7 +3963,7 @@ pub struct CancelMaintenanceResponse {
|
|||
/// nedtellingstasken.
|
||||
pub async fn cancel_maintenance(
|
||||
State(state): State<AppState>,
|
||||
_user: AuthUser,
|
||||
_admin: AdminUser,
|
||||
) -> Result<Json<CancelMaintenanceResponse>, (StatusCode, Json<ErrorResponse>)> {
|
||||
state
|
||||
.maintenance
|
||||
|
|
@ -3980,7 +3980,7 @@ pub async fn cancel_maintenance(
|
|||
/// Brukes av admin-panelet for å vise aktive sesjoner før bekreftelse.
|
||||
pub async fn maintenance_status(
|
||||
State(state): State<AppState>,
|
||||
_user: AuthUser,
|
||||
_admin: AdminUser,
|
||||
) -> Result<Json<crate::maintenance::MaintenanceStatus>, (StatusCode, Json<ErrorResponse>)> {
|
||||
let status = state
|
||||
.maintenance
|
||||
|
|
@ -4001,7 +4001,7 @@ pub async fn maintenance_status(
|
|||
/// Query-params: ?status=error&type=whisper_transcribe&collection_id=...&limit=50&offset=0
|
||||
pub async fn list_jobs(
|
||||
State(state): State<AppState>,
|
||||
_user: AuthUser,
|
||||
_admin: AdminUser,
|
||||
axum::extract::Query(params): axum::extract::Query<ListJobsParams>,
|
||||
) -> Result<Json<ListJobsResponse>, (StatusCode, Json<ErrorResponse>)> {
|
||||
let limit = params.limit.unwrap_or(50).min(200);
|
||||
|
|
@ -4050,7 +4050,7 @@ pub struct ListJobsResponse {
|
|||
/// Sett en feilet jobb tilbake til 'pending' for nytt forsøk.
|
||||
pub async fn retry_job(
|
||||
State(state): State<AppState>,
|
||||
_user: AuthUser,
|
||||
_admin: AdminUser,
|
||||
Json(req): Json<JobIdRequest>,
|
||||
) -> Result<Json<JobActionResponse>, (StatusCode, Json<ErrorResponse>)> {
|
||||
let retried = crate::jobs::retry_job(&state.db, req.job_id)
|
||||
|
|
@ -4061,7 +4061,7 @@ pub async fn retry_job(
|
|||
return Err(bad_request("Jobben finnes ikke eller har feil status for retry"));
|
||||
}
|
||||
|
||||
tracing::info!(job_id = %req.job_id, user = %_user.node_id, "Admin: jobb restartet");
|
||||
tracing::info!(job_id = %req.job_id, user = %_admin.node_id, "Admin: jobb restartet");
|
||||
Ok(Json(JobActionResponse { success: true }))
|
||||
}
|
||||
|
||||
|
|
@ -4070,7 +4070,7 @@ pub async fn retry_job(
|
|||
/// Avbryt en ventende jobb.
|
||||
pub async fn cancel_job(
|
||||
State(state): State<AppState>,
|
||||
_user: AuthUser,
|
||||
_admin: AdminUser,
|
||||
Json(req): Json<JobIdRequest>,
|
||||
) -> Result<Json<JobActionResponse>, (StatusCode, Json<ErrorResponse>)> {
|
||||
let cancelled = crate::jobs::cancel_job(&state.db, req.job_id)
|
||||
|
|
@ -4081,7 +4081,7 @@ pub async fn cancel_job(
|
|||
return Err(bad_request("Jobben finnes ikke eller har feil status for avbryt"));
|
||||
}
|
||||
|
||||
tracing::info!(job_id = %req.job_id, user = %_user.node_id, "Admin: jobb avbrutt");
|
||||
tracing::info!(job_id = %req.job_id, user = %_admin.node_id, "Admin: jobb avbrutt");
|
||||
Ok(Json(JobActionResponse { success: true }))
|
||||
}
|
||||
|
||||
|
|
@ -4102,7 +4102,7 @@ pub struct JobActionResponse {
|
|||
/// GET /admin/resources — samlet ressursstatus.
|
||||
pub async fn resource_status(
|
||||
State(state): State<AppState>,
|
||||
_user: AuthUser,
|
||||
_admin: AdminUser,
|
||||
) -> Result<Json<crate::resources::ResourceStatus>, (StatusCode, Json<ErrorResponse>)> {
|
||||
let cas_root = std::env::var("CAS_ROOT")
|
||||
.unwrap_or_else(|_| "/srv/synops/media/cas".to_string());
|
||||
|
|
@ -4139,7 +4139,7 @@ pub async fn resource_status(
|
|||
/// GET /admin/resources/disk — disk-status med historikk.
|
||||
pub async fn resource_disk(
|
||||
State(state): State<AppState>,
|
||||
_user: AuthUser,
|
||||
_admin: AdminUser,
|
||||
) -> Result<Json<DiskOverview>, (StatusCode, Json<ErrorResponse>)> {
|
||||
let current = crate::resources::latest_disk_status(&state.db)
|
||||
.await
|
||||
|
|
@ -4160,7 +4160,7 @@ pub struct DiskOverview {
|
|||
/// POST /admin/resources/update_rule — oppdater en prioritetsregel.
|
||||
pub async fn update_priority_rule(
|
||||
State(state): State<AppState>,
|
||||
_user: AuthUser,
|
||||
_admin: AdminUser,
|
||||
Json(req): Json<UpdatePriorityRuleRequest>,
|
||||
) -> Result<Json<JobActionResponse>, (StatusCode, Json<ErrorResponse>)> {
|
||||
sqlx::query(
|
||||
|
|
@ -4192,7 +4192,7 @@ pub async fn update_priority_rule(
|
|||
|
||||
tracing::info!(
|
||||
job_type = %req.job_type,
|
||||
user = %_user.node_id,
|
||||
user = %_admin.node_id,
|
||||
"Admin: prioritetsregel oppdatert"
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -79,15 +79,16 @@ pub struct SetGainRequest {
|
|||
|
||||
pub async fn set_gain(
|
||||
State(state): State<AppState>,
|
||||
_user: AuthUser,
|
||||
user: AuthUser,
|
||||
Json(req): Json<SetGainRequest>,
|
||||
) -> Result<Json<MixerChannelResponse>, (StatusCode, Json<ErrorResponse>)> {
|
||||
sqlx::query(
|
||||
"UPDATE mixer_channels SET gain = $1, updated_at = now() WHERE room_id = $2 AND target_user_id = $3"
|
||||
"UPDATE mixer_channels SET gain = $1, updated_by = $4, updated_at = now() WHERE room_id = $2 AND target_user_id = $3"
|
||||
)
|
||||
.bind(req.gain.clamp(0.0, 1.5))
|
||||
.bind(&req.room_id)
|
||||
.bind(&req.target_user_id)
|
||||
.bind(user.node_id.to_string())
|
||||
.execute(&state.db)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
|
|
@ -111,15 +112,16 @@ pub struct SetMuteRequest {
|
|||
|
||||
pub async fn set_mute(
|
||||
State(state): State<AppState>,
|
||||
_user: AuthUser,
|
||||
user: AuthUser,
|
||||
Json(req): Json<SetMuteRequest>,
|
||||
) -> Result<Json<MixerChannelResponse>, (StatusCode, Json<ErrorResponse>)> {
|
||||
sqlx::query(
|
||||
"UPDATE mixer_channels SET is_muted = $1, updated_at = now() WHERE room_id = $2 AND target_user_id = $3"
|
||||
"UPDATE mixer_channels SET is_muted = $1, updated_by = $4, updated_at = now() WHERE room_id = $2 AND target_user_id = $3"
|
||||
)
|
||||
.bind(req.is_muted)
|
||||
.bind(&req.room_id)
|
||||
.bind(&req.target_user_id)
|
||||
.bind(user.node_id.to_string())
|
||||
.execute(&state.db)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
|
|
@ -143,42 +145,32 @@ pub struct ToggleEffectRequest {
|
|||
|
||||
pub async fn toggle_effect(
|
||||
State(state): State<AppState>,
|
||||
_user: AuthUser,
|
||||
user: AuthUser,
|
||||
Json(req): Json<ToggleEffectRequest>,
|
||||
) -> Result<Json<MixerChannelResponse>, (StatusCode, Json<ErrorResponse>)> {
|
||||
// Hent nåværende active_effects JSON
|
||||
let current: Option<String> = sqlx::query_scalar(
|
||||
"SELECT active_effects FROM mixer_channels WHERE room_id = $1 AND target_user_id = $2"
|
||||
)
|
||||
.bind(&req.room_id)
|
||||
.bind(&req.target_user_id)
|
||||
.fetch_optional(&state.db)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
tracing::error!("Feil ved toggle_effect (les): {e}");
|
||||
internal_error("Databasefeil")
|
||||
})?;
|
||||
|
||||
let mut effects: serde_json::Value = current
|
||||
.and_then(|s| serde_json::from_str(&s).ok())
|
||||
.unwrap_or(serde_json::json!({}));
|
||||
|
||||
// Toggle effekten
|
||||
let current_val = effects.get(&req.effect_name).and_then(|v| v.as_bool()).unwrap_or(false);
|
||||
effects[&req.effect_name] = serde_json::Value::Bool(!current_val);
|
||||
|
||||
let new_effects = serde_json::to_string(&effects).unwrap_or_else(|_| "{}".to_string());
|
||||
|
||||
// Atomisk toggle via PG JSON-operasjoner — unngår race condition ved
|
||||
// samtidige oppdateringer fra flere klienter.
|
||||
sqlx::query(
|
||||
"UPDATE mixer_channels SET active_effects = $1, updated_at = now() WHERE room_id = $2 AND target_user_id = $3"
|
||||
r#"UPDATE mixer_channels
|
||||
SET active_effects = (
|
||||
CASE
|
||||
WHEN (active_effects::jsonb->>$3)::boolean IS NOT DISTINCT FROM true
|
||||
THEN (active_effects::jsonb || jsonb_build_object($3, false))::text
|
||||
ELSE (active_effects::jsonb || jsonb_build_object($3, true))::text
|
||||
END
|
||||
),
|
||||
updated_by = $4,
|
||||
updated_at = now()
|
||||
WHERE room_id = $1 AND target_user_id = $2"#,
|
||||
)
|
||||
.bind(&new_effects)
|
||||
.bind(&req.room_id)
|
||||
.bind(&req.target_user_id)
|
||||
.bind(&req.effect_name)
|
||||
.bind(user.node_id.to_string())
|
||||
.execute(&state.db)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
tracing::error!("Feil ved toggle_effect (skriv): {e}");
|
||||
tracing::error!("Feil ved toggle_effect: {e}");
|
||||
internal_error("Databasefeil")
|
||||
})?;
|
||||
|
||||
|
|
@ -198,7 +190,7 @@ pub struct SetMixerRoleRequest {
|
|||
|
||||
pub async fn set_mixer_role(
|
||||
State(state): State<AppState>,
|
||||
_user: AuthUser,
|
||||
user: AuthUser,
|
||||
Json(req): Json<SetMixerRoleRequest>,
|
||||
) -> Result<Json<MixerChannelResponse>, (StatusCode, Json<ErrorResponse>)> {
|
||||
let valid_roles = ["editor", "viewer"];
|
||||
|
|
@ -207,11 +199,12 @@ pub async fn set_mixer_role(
|
|||
}
|
||||
|
||||
sqlx::query(
|
||||
"UPDATE mixer_channels SET role = $1, updated_at = now() WHERE room_id = $2 AND target_user_id = $3"
|
||||
"UPDATE mixer_channels SET role = $1, updated_by = $4, updated_at = now() WHERE room_id = $2 AND target_user_id = $3"
|
||||
)
|
||||
.bind(&req.role)
|
||||
.bind(&req.room_id)
|
||||
.bind(&req.target_user_id)
|
||||
.bind(user.node_id.to_string())
|
||||
.execute(&state.db)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ use serde::{Deserialize, Serialize};
|
|||
use sqlx::PgPool;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::auth::AuthUser;
|
||||
use crate::auth::AdminUser;
|
||||
use crate::AppState;
|
||||
|
||||
// =============================================================================
|
||||
|
|
@ -90,7 +90,7 @@ fn internal_error(msg: &str) -> (StatusCode, Json<ErrorResponse>) {
|
|||
|
||||
pub async fn usage_overview(
|
||||
State(state): State<AppState>,
|
||||
_user: AuthUser,
|
||||
_admin: AdminUser,
|
||||
axum::extract::Query(params): axum::extract::Query<UsageOverviewParams>,
|
||||
) -> Result<Json<UsageOverviewResponse>, (StatusCode, Json<ErrorResponse>)> {
|
||||
let days = params.days.unwrap_or(30).clamp(1, 365);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue