Skip to main content

wings_api/
extra.rs

1use garde::Validate;
2use serde::{Deserialize, Serialize};
3use utoipa::ToSchema;
4
5pub type Config = super::system_config::get::Response200;
6
7#[derive(Debug, ToSchema, Deserialize, Serialize, Clone)]
8#[serde(tag = "type", rename_all = "snake_case")]
9pub enum QueryValue {
10    Null,
11    Text { value: String },
12    Binary { value: String },
13}
14
15#[derive(Debug, ToSchema, Deserialize, Serialize, Clone)]
16#[serde(tag = "type", rename_all = "snake_case")]
17#[non_exhaustive]
18pub enum ServerSelector {
19    Uuids {
20        uuids: std::collections::HashSet<uuid::Uuid>,
21    },
22    All,
23}
24
25impl std::fmt::Display for super::StreamableArchiveFormat {
26    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
27        write!(
28            f,
29            "{}",
30            match self {
31                super::StreamableArchiveFormat::Tar => "tar",
32                super::StreamableArchiveFormat::TarGz => "tar_gz",
33                super::StreamableArchiveFormat::TarXz => "tar_xz",
34                super::StreamableArchiveFormat::TarLzip => "tar_lzip",
35                super::StreamableArchiveFormat::TarBz2 => "tar_bz2",
36                super::StreamableArchiveFormat::TarLz4 => "tar_lz4",
37                super::StreamableArchiveFormat::TarZstd => "tar_zstd",
38                super::StreamableArchiveFormat::Itaf => "itaf",
39                super::StreamableArchiveFormat::ItafGz => "itaf_gz",
40                super::StreamableArchiveFormat::ItafXz => "itaf_xz",
41                super::StreamableArchiveFormat::ItafLzip => "itaf_lzip",
42                super::StreamableArchiveFormat::ItafBz2 => "itaf_bz2",
43                super::StreamableArchiveFormat::ItafLz4 => "itaf_lz4",
44                super::StreamableArchiveFormat::ItafZstd => "itaf_zstd",
45                super::StreamableArchiveFormat::Zip => "zip",
46            }
47        )
48    }
49}
50
51#[allow(clippy::derivable_impls)]
52impl Default for super::StreamableArchiveFormat {
53    #[inline]
54    fn default() -> Self {
55        super::StreamableArchiveFormat::TarGz
56    }
57}
58
59impl std::fmt::Display for super::BackupAdapter {
60    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
61        write!(
62            f,
63            "{}",
64            match self {
65                super::BackupAdapter::Wings => "wings",
66                super::BackupAdapter::S3 => "s3",
67                super::BackupAdapter::DdupBak => "ddup-bak",
68                super::BackupAdapter::Btrfs => "btrfs",
69                super::BackupAdapter::Zfs => "zfs",
70                super::BackupAdapter::Restic => "restic",
71                super::BackupAdapter::ProxmoxBackupServer => "proxmox-backup-server",
72                super::BackupAdapter::Kopia => "kopia",
73            }
74        )
75    }
76}
77
78#[derive(Debug, ToSchema, Deserialize, Serialize, Clone, Copy)]
79#[serde(rename_all = "lowercase")]
80#[non_exhaustive]
81pub enum Algorithm {
82    Md5,
83    Crc32,
84    Sha1,
85    Sha224,
86    Sha256,
87    Sha384,
88    Sha512,
89    Curseforge,
90}
91
92impl std::fmt::Display for Algorithm {
93    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
94        write!(
95            f,
96            "{}",
97            match self {
98                Algorithm::Md5 => "md5",
99                Algorithm::Crc32 => "crc32",
100                Algorithm::Sha1 => "sha1",
101                Algorithm::Sha224 => "sha224",
102                Algorithm::Sha256 => "sha256",
103                Algorithm::Sha384 => "sha384",
104                Algorithm::Sha512 => "sha512",
105                Algorithm::Curseforge => "curseforge",
106            }
107        )
108    }
109}
110
111#[derive(Debug, Default, ToSchema, Deserialize, Serialize, Clone, Copy)]
112#[serde(rename_all = "snake_case")]
113#[non_exhaustive]
114pub enum DirectorySortingMode {
115    #[default]
116    NameAsc,
117    NameDesc,
118    SizeAsc,
119    SizeDesc,
120    PhysicalSizeAsc,
121    PhysicalSizeDesc,
122    ModifiedAsc,
123    ModifiedDesc,
124    CreatedAsc,
125    CreatedDesc,
126}
127
128impl std::fmt::Display for DirectorySortingMode {
129    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
130        write!(
131            f,
132            "{}",
133            match self {
134                DirectorySortingMode::NameAsc => "name_asc",
135                DirectorySortingMode::NameDesc => "name_desc",
136                DirectorySortingMode::SizeAsc => "size_asc",
137                DirectorySortingMode::SizeDesc => "size_desc",
138                DirectorySortingMode::PhysicalSizeAsc => "physical_size_asc",
139                DirectorySortingMode::PhysicalSizeDesc => "physical_size_desc",
140                DirectorySortingMode::ModifiedAsc => "modified_asc",
141                DirectorySortingMode::ModifiedDesc => "modified_desc",
142                DirectorySortingMode::CreatedAsc => "created_asc",
143                DirectorySortingMode::CreatedDesc => "created_desc",
144            }
145        )
146    }
147}
148
149#[derive(ToSchema, Debug, Clone, Copy, Deserialize, Serialize)]
150#[serde(rename_all = "lowercase")]
151pub enum ServerBackupStatus {
152    Starting,
153    Finished,
154    Failed,
155}
156
157#[derive(Debug, ToSchema, Deserialize, Serialize, Clone, Copy)]
158#[serde(rename_all = "snake_case")]
159#[non_exhaustive]
160pub enum Game {
161    MinecraftJava,
162}
163
164impl std::fmt::Display for Game {
165    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
166        write!(
167            f,
168            "{}",
169            match self {
170                Game::MinecraftJava => "minecraft_java",
171            }
172        )
173    }
174}
175
176#[derive(ToSchema, Clone, Deserialize, Serialize, Validate)]
177pub struct ScheduleVariable {
178    #[garde(length(chars, min = 1, max = 255))]
179    #[schema(min_length = 1, max_length = 255)]
180    pub variable: compact_str::CompactString,
181}
182
183#[derive(ToSchema, Clone, Deserialize, Serialize, Validate)]
184#[serde(untagged)]
185pub enum ScheduleDynamicParameter {
186    Raw(#[garde(length(chars, min = 1, max = 16384))] compact_str::CompactString),
187    Variable(#[garde(dive)] ScheduleVariable),
188}
189
190#[derive(ToSchema, Deserialize, Serialize, Clone, Copy)]
191#[serde(rename_all = "snake_case")]
192pub enum ScheduleHttpMethod {
193    Get,
194    Post,
195    Put,
196    Patch,
197    Delete,
198    Head,
199}
200
201#[derive(ToSchema, Clone, Deserialize, Serialize, Validate)]
202pub struct ScheduleHttpHeader {
203    #[garde(length(chars, min = 1, max = 128))]
204    #[schema(min_length = 1, max_length = 128)]
205    pub name: compact_str::CompactString,
206    #[garde(dive)]
207    pub value: ScheduleDynamicParameter,
208}
209
210#[derive(ToSchema, Deserialize, Serialize)]
211pub struct ScheduleAction {
212    pub uuid: uuid::Uuid,
213
214    #[serde(flatten)]
215    pub inner: ScheduleActionInner,
216}
217
218#[derive(ToSchema, Validate, Deserialize, Serialize, Clone)]
219#[serde(rename_all = "snake_case", tag = "mode")]
220#[non_exhaustive]
221pub enum ScheduleBackupSelector {
222    Latest {
223        #[garde(skip)]
224        #[serde(default)]
225        backup_group_uuid: Option<uuid::Uuid>,
226    },
227    Oldest {
228        #[garde(skip)]
229        #[serde(default)]
230        backup_group_uuid: Option<uuid::Uuid>,
231    },
232    Uuid {
233        #[garde(dive)]
234        uuid: ScheduleDynamicParameter,
235    },
236    Name {
237        #[garde(dive)]
238        name: ScheduleDynamicParameter,
239        #[garde(skip)]
240        #[serde(default)]
241        backup_group_uuid: Option<uuid::Uuid>,
242        #[garde(skip)]
243        #[serde(default)]
244        oldest: bool,
245    },
246}
247
248#[derive(ToSchema, Validate, Deserialize, Serialize, Clone)]
249#[serde(rename_all = "snake_case", tag = "type")]
250#[non_exhaustive]
251pub enum ScheduleActionInner {
252    Sleep {
253        #[garde(range(min = 1, max = 24 * 60 * 60 * 1000))]
254        #[schema(minimum = 1, maximum = 86400000)]
255        duration: u64,
256    },
257    Ensure {
258        #[garde(dive, custom(ScheduleCondition::validate_nesting))]
259        condition: ScheduleCondition,
260    },
261    If {
262        #[garde(dive, custom(ScheduleCondition::validate_nesting))]
263        condition: ScheduleCondition,
264    },
265    ElseIf {
266        #[garde(dive, custom(ScheduleCondition::validate_nesting))]
267        condition: ScheduleCondition,
268    },
269    Else,
270    EndIf,
271    Exit {
272        #[garde(skip)]
273        successful: bool,
274    },
275    WaitForState {
276        #[garde(skip)]
277        ignore_failure: bool,
278
279        #[garde(skip)]
280        state: super::ServerState,
281        #[garde(range(min = 1, max = 24 * 60 * 60 * 1000))]
282        #[schema(minimum = 1, maximum = 86400000)]
283        timeout: u64,
284    },
285    Format {
286        #[garde(length(chars, min = 1, max = 16384))]
287        #[schema(min_length = 1, max_length = 16384)]
288        format: String,
289        #[garde(dive)]
290        output_into: ScheduleVariable,
291    },
292    MatchRegex {
293        #[garde(dive)]
294        input: ScheduleDynamicParameter,
295
296        #[garde(skip)]
297        #[serde(with = "serde_regex")]
298        #[schema(value_type = String, format = "regex")]
299        regex: regex::Regex,
300
301        #[garde(skip)]
302        output_into: Vec<Option<ScheduleVariable>>,
303    },
304    WaitForConsoleLine {
305        #[garde(skip)]
306        ignore_failure: bool,
307
308        #[garde(dive)]
309        contains: ScheduleDynamicParameter,
310        #[garde(skip)]
311        #[serde(default)]
312        case_insensitive: bool,
313        #[garde(range(min = 1, max = 24 * 60 * 60 * 1000))]
314        #[schema(minimum = 1, maximum = 86400000)]
315        timeout: u64,
316
317        #[garde(dive)]
318        output_into: Option<ScheduleVariable>,
319    },
320    SendPower {
321        #[garde(skip)]
322        ignore_failure: bool,
323
324        #[garde(skip)]
325        action: super::ServerPowerAction,
326    },
327    SendCommand {
328        #[garde(skip)]
329        ignore_failure: bool,
330
331        #[garde(dive)]
332        command: ScheduleDynamicParameter,
333    },
334    CreateBackup {
335        #[garde(skip)]
336        ignore_failure: bool,
337        #[garde(skip)]
338        foreground: bool,
339
340        #[garde(dive)]
341        name: Option<ScheduleDynamicParameter>,
342        #[garde(skip)]
343        #[serde(default)]
344        backup_group_uuid: Option<uuid::Uuid>,
345        #[garde(skip)]
346        ignored_files: Vec<compact_str::CompactString>,
347
348        #[garde(dive)]
349        #[serde(default)]
350        output_into: Option<ScheduleVariable>,
351    },
352    RestoreBackup {
353        #[garde(skip)]
354        ignore_failure: bool,
355        #[garde(skip)]
356        truncate_directory: bool,
357        #[garde(skip)]
358        #[serde(default)]
359        restore_startup: bool,
360
361        #[garde(dive)]
362        backup: ScheduleBackupSelector,
363    },
364    DeleteBackup {
365        #[garde(skip)]
366        #[serde(default)]
367        ignore_failure: bool,
368
369        #[garde(dive)]
370        backup: ScheduleBackupSelector,
371    },
372    MoveBackup {
373        #[garde(skip)]
374        #[serde(default)]
375        ignore_failure: bool,
376
377        #[garde(dive)]
378        backup: ScheduleBackupSelector,
379        #[garde(skip)]
380        #[serde(default)]
381        backup_group_uuid: Option<uuid::Uuid>,
382    },
383    CreateDirectory {
384        #[garde(skip)]
385        ignore_failure: bool,
386
387        #[garde(dive)]
388        root: ScheduleDynamicParameter,
389        #[garde(dive)]
390        name: ScheduleDynamicParameter,
391    },
392    WriteFile {
393        #[garde(skip)]
394        ignore_failure: bool,
395        #[garde(skip)]
396        append: bool,
397
398        #[garde(dive)]
399        file: ScheduleDynamicParameter,
400        #[garde(dive)]
401        content: ScheduleDynamicParameter,
402    },
403    CopyFile {
404        #[garde(skip)]
405        ignore_failure: bool,
406        #[garde(skip)]
407        foreground: bool,
408
409        #[garde(dive)]
410        file: ScheduleDynamicParameter,
411        #[garde(dive)]
412        destination: ScheduleDynamicParameter,
413    },
414    DeleteFiles {
415        #[garde(skip)]
416        #[serde(default)]
417        ignore_failure: bool,
418
419        #[garde(dive)]
420        root: ScheduleDynamicParameter,
421        #[garde(skip)]
422        files: Vec<compact_str::CompactString>,
423    },
424    RenameFiles {
425        #[garde(skip)]
426        #[serde(default)]
427        ignore_failure: bool,
428
429        #[garde(dive)]
430        root: ScheduleDynamicParameter,
431        #[garde(skip)]
432        #[schema(inline)]
433        files: Vec<super::servers_server_files_rename::put::RequestBodyFiles>,
434    },
435    CompressFiles {
436        #[garde(skip)]
437        ignore_failure: bool,
438        #[garde(skip)]
439        foreground: bool,
440
441        #[garde(dive)]
442        root: ScheduleDynamicParameter,
443        #[garde(skip)]
444        files: Vec<compact_str::CompactString>,
445        #[garde(skip)]
446        format: super::ArchiveFormat,
447        #[garde(dive)]
448        name: ScheduleDynamicParameter,
449    },
450    DecompressFile {
451        #[garde(skip)]
452        ignore_failure: bool,
453        #[garde(skip)]
454        foreground: bool,
455
456        #[garde(dive)]
457        root: ScheduleDynamicParameter,
458        #[garde(dive)]
459        file: ScheduleDynamicParameter,
460    },
461    UpdateStartupVariable {
462        #[garde(skip)]
463        ignore_failure: bool,
464
465        #[garde(dive)]
466        env_variable: ScheduleDynamicParameter,
467        #[garde(dive)]
468        value: ScheduleDynamicParameter,
469    },
470    UpdateStartupCommand {
471        #[garde(skip)]
472        ignore_failure: bool,
473
474        #[garde(dive)]
475        command: ScheduleDynamicParameter,
476    },
477    UpdateStartupDockerImage {
478        #[garde(skip)]
479        ignore_failure: bool,
480
481        #[garde(dive)]
482        image: ScheduleDynamicParameter,
483    },
484    HttpRequest {
485        #[garde(skip)]
486        ignore_failure: bool,
487
488        #[garde(skip)]
489        method: ScheduleHttpMethod,
490        #[garde(skip)]
491        #[schema(value_type = String, format = "uri")]
492        url: reqwest::Url,
493        #[garde(length(max = 32), dive)]
494        #[serde(default)]
495        headers: Vec<ScheduleHttpHeader>,
496        #[garde(dive)]
497        #[serde(default)]
498        body: Option<ScheduleDynamicParameter>,
499        #[garde(range(min = 1, max = 60 * 1000))]
500        #[schema(minimum = 1, maximum = 60000)]
501        timeout: u64,
502        #[garde(skip)]
503        #[serde(default)]
504        ignore_error_status: bool,
505
506        #[garde(dive)]
507        #[serde(default)]
508        output_status_into: Option<ScheduleVariable>,
509        #[garde(dive)]
510        #[serde(default)]
511        output_body_into: Option<ScheduleVariable>,
512    },
513}
514
515impl ScheduleActionInner {
516    pub fn permission(&self) -> Option<&'static str> {
517        match self {
518            ScheduleActionInner::Sleep { .. } => None,
519            ScheduleActionInner::Ensure { .. } => None,
520            ScheduleActionInner::If { .. } => None,
521            ScheduleActionInner::ElseIf { .. } => None,
522            ScheduleActionInner::Else => None,
523            ScheduleActionInner::EndIf => None,
524            ScheduleActionInner::Exit { .. } => None,
525            ScheduleActionInner::WaitForState { .. } => None,
526            ScheduleActionInner::Format { .. } => None,
527            ScheduleActionInner::MatchRegex { .. } => None,
528            ScheduleActionInner::WaitForConsoleLine { .. } => Some("control.read-console"),
529            ScheduleActionInner::SendPower { action, .. } => match action {
530                super::ServerPowerAction::Start => Some("control.start"),
531                super::ServerPowerAction::Stop => Some("control.stop"),
532                super::ServerPowerAction::Restart => Some("control.restart"),
533                super::ServerPowerAction::Kill => Some("control.stop"),
534            },
535            ScheduleActionInner::SendCommand { .. } => Some("control.console"),
536            ScheduleActionInner::CreateBackup { .. } => Some("backups.create"),
537            ScheduleActionInner::RestoreBackup { .. } => Some("backups.restore"),
538            ScheduleActionInner::DeleteBackup { .. } => Some("backups.delete"),
539            ScheduleActionInner::MoveBackup { .. } => Some("backups.update"),
540            ScheduleActionInner::CreateDirectory { .. } => Some("files.create"),
541            ScheduleActionInner::WriteFile { .. } => Some("files.update"),
542            ScheduleActionInner::CopyFile { .. } => Some("files.update"),
543            ScheduleActionInner::DeleteFiles { .. } => Some("files.delete"),
544            ScheduleActionInner::RenameFiles { .. } => Some("files.update"),
545            ScheduleActionInner::CompressFiles { .. } => Some("files.archive"),
546            ScheduleActionInner::DecompressFile { .. } => Some("files.archive"),
547            ScheduleActionInner::UpdateStartupVariable { .. } => Some("startup.update"),
548            ScheduleActionInner::UpdateStartupCommand { .. } => Some("startup.command"),
549            ScheduleActionInner::UpdateStartupDockerImage { .. } => Some("startup.docker-image"),
550            ScheduleActionInner::HttpRequest { .. } => None,
551        }
552    }
553}
554
555#[derive(ToSchema, Validate, Deserialize, Serialize, Clone)]
556#[serde(rename_all = "snake_case", tag = "type")]
557#[non_exhaustive]
558pub enum ScheduleTrigger {
559    Cron {
560        #[garde(skip)]
561        #[schema(value_type = String, example = "* * * * * *")]
562        schedule: Box<croner::Cron>,
563    },
564    PowerAction {
565        #[garde(skip)]
566        action: super::ServerPowerAction,
567    },
568    ServerState {
569        #[garde(skip)]
570        state: super::ServerState,
571    },
572    BackupStatus {
573        #[garde(skip)]
574        status: ServerBackupStatus,
575    },
576    ScheduleCompletion {
577        #[garde(skip)]
578        schedule: uuid::Uuid,
579        #[garde(skip)]
580        successful: bool,
581    },
582    ResourceUsage {
583        #[garde(skip)]
584        metric: ScheduleResourceMetric,
585        #[garde(skip)]
586        comparator: ScheduleConditionComparator,
587        #[garde(range(min = 0.0))]
588        value: f64,
589        #[garde(range(min = 0, max = 24 * 60 * 60))]
590        #[schema(minimum = 0, maximum = 86400)]
591        #[serde(default)]
592        for_seconds: u64,
593    },
594    ConsoleLine {
595        #[garde(length(chars, min = 1, max = 1024))]
596        #[schema(min_length = 1, max_length = 1024)]
597        contains: compact_str::CompactString,
598        #[garde(skip)]
599        #[serde(default)]
600        case_insensitive: bool,
601        #[garde(dive)]
602        output_into: Option<ScheduleVariable>,
603    },
604    Crash,
605}
606
607#[derive(ToSchema, Deserialize, Serialize, Clone, Copy)]
608#[serde(rename_all = "snake_case")]
609pub enum ScheduleConditionComparator {
610    SmallerThan,
611    SmallerThanOrEquals,
612    Equal,
613    GreaterThan,
614    GreaterThanOrEquals,
615}
616
617#[derive(ToSchema, Deserialize, Serialize, Clone, Copy)]
618#[serde(rename_all = "snake_case")]
619pub enum ScheduleResourceMetric {
620    Cpu,
621    Memory,
622    Disk,
623}
624
625#[derive(ToSchema, Deserialize, Serialize, Clone, Validate)]
626#[serde(rename_all = "snake_case", tag = "type")]
627#[schema(no_recursion)]
628pub enum ScheduleCondition {
629    None,
630    And {
631        #[garde(dive)]
632        conditions: Vec<ScheduleCondition>,
633    },
634    Or {
635        #[garde(dive)]
636        conditions: Vec<ScheduleCondition>,
637    },
638    Not {
639        #[garde(dive)]
640        condition: Box<ScheduleCondition>,
641    },
642    ServerState {
643        #[garde(skip)]
644        state: super::ServerState,
645    },
646    Uptime {
647        #[garde(skip)]
648        comparator: ScheduleConditionComparator,
649        #[garde(skip)]
650        value: u64,
651    },
652    ResourceUsage {
653        #[garde(skip)]
654        metric: ScheduleResourceMetric,
655        #[garde(skip)]
656        comparator: ScheduleConditionComparator,
657        #[garde(range(min = 0.0))]
658        value: f64,
659    },
660    FileExists {
661        #[garde(length(chars, min = 1, max = 255))]
662        #[schema(min_length = 1, max_length = 255)]
663        file: String,
664    },
665    VariableExists {
666        #[garde(dive)]
667        variable: ScheduleVariable,
668    },
669    VariableEquals {
670        #[garde(dive)]
671        variable: ScheduleVariable,
672        #[garde(dive)]
673        equals: ScheduleDynamicParameter,
674    },
675    VariableContains {
676        #[garde(dive)]
677        variable: ScheduleVariable,
678        #[garde(dive)]
679        contains: ScheduleDynamicParameter,
680    },
681    VariableStartsWith {
682        #[garde(dive)]
683        variable: ScheduleVariable,
684        #[garde(dive)]
685        starts_with: ScheduleDynamicParameter,
686    },
687    VariableEndsWith {
688        #[garde(dive)]
689        variable: ScheduleVariable,
690        #[garde(dive)]
691        ends_with: ScheduleDynamicParameter,
692    },
693}
694
695impl ScheduleCondition {
696    pub const MAX_NESTING_DEPTH: usize = 3;
697
698    fn nested_within_limit(&self, depth: usize) -> bool {
699        match self {
700            ScheduleCondition::And { conditions } | ScheduleCondition::Or { conditions } => {
701                depth < Self::MAX_NESTING_DEPTH
702                    && conditions.iter().all(|c| c.nested_within_limit(depth + 1))
703            }
704            ScheduleCondition::Not { condition } => {
705                depth < Self::MAX_NESTING_DEPTH && condition.nested_within_limit(depth + 1)
706            }
707            _ => true,
708        }
709    }
710
711    pub fn validate_nesting(value: &Self, _context: &()) -> garde::Result {
712        if value.nested_within_limit(0) {
713            Ok(())
714        } else {
715            Err(garde::Error::new(format!(
716                "condition may not nest groups more than {} levels deep",
717                Self::MAX_NESTING_DEPTH
718            )))
719        }
720    }
721
722    pub fn validate_optional_nesting(value: &Option<Self>, context: &()) -> garde::Result {
723        match value {
724            Some(condition) => Self::validate_nesting(condition, context),
725            None => Ok(()),
726        }
727    }
728}
729
730// mirrors wings config::FORBIDDEN_PATHS
731const FORBIDDEN_CONFIG_PATHS: &[&str] = &[
732    "uuid",
733    "token",
734    "token_id",
735    "remote",
736    "remote_headers",
737    "system.root_directory",
738    "system.log_directory",
739    "system.data",
740    "system.diffs_directory",
741    "system.vmount_directory",
742    "system.archive_directory",
743    "system.backup_directory",
744    "system.tmp_directory",
745    "system.passwd.directory",
746    "system.backups.restic.repository",
747    "system.backups.restic.password_file",
748    "system.backups.mounting.path",
749    "system.username",
750    "system.user",
751    "system.passwd",
752    "docker.socket",
753    "allowed_mounts",
754    "ignore_panel_config_updates",
755    "ignore_panel_wings_upgrades",
756    "api.host",
757    "api.port",
758    "api.ssl",
759    "api.trusted_proxies",
760    "api.disable_remote_download",
761    "api.remote_download_blocked_cidrs",
762    "api.schedule.steps.http_request",
763];
764
765pub fn strip_config_paths(value: &mut serde_json::Value) {
766    for path in FORBIDDEN_CONFIG_PATHS {
767        let mut cursor = &mut *value;
768        let mut parts = path.split('.').peekable();
769
770        while let Some(part) = parts.next() {
771            let serde_json::Value::Object(map) = cursor else {
772                break;
773            };
774
775            if parts.peek().is_none() {
776                map.remove(part);
777                break;
778            }
779
780            if map.get(part).is_some_and(|next| !next.is_object()) {
781                map.remove(part);
782                break;
783            }
784
785            match map.get_mut(part) {
786                Some(next) => cursor = next,
787                None => break,
788            }
789        }
790    }
791}