From f589892251cd7b35b542b3b8df63f3f1609152e3 Mon Sep 17 00:00:00 2001 From: hhftechnologies Date: Sun, 10 May 2026 16:08:47 +0530 Subject: [PATCH 1/5] Add base64 profiles support & mobile UI updates MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Server: add base64 profile decoding utilities and errors, extend ProfileRequest with content_b64 and encoding, and use decoded content when saving profiles and creating history; add unit tests for decodeProfileContent. Web: add UTF-8 → base64 encoder and payload builder, switch profiles API to send base64 payloads, and add tests for encoding/building payloads; add an error rule to surface 403s when saving profiles. Mobile: bump Capacitor deps to v8 and iOS platform to 15.0, add Android edge-to-edge support (WindowCompat.setDecorFitsSystemWindows + transparent system bars and updated styles), remove deprecated overlaysWebView/backgroundColor calls, update main.tsx comments, and add a placeholder Android test file. --- internal/api/handlers/profiles.go | 60 ++++- internal/api/handlers/profiles_test.go | 88 ++++++ internal/models/models.go | 6 +- mobile/android/MainActivity.test.java | 4 + .../crowdsec/manager/mobile/MainActivity.java | 2 + .../app/src/main/res/values/styles.xml | 6 +- mobile/capacitor.config.ts | 2 - mobile/ios/App/Podfile | 2 +- mobile/package-lock.json | 253 ++++++++++++------ mobile/package.json | 20 +- mobile/src/main.tsx | 8 +- web/package-lock.json | 4 +- web/package.json | 3 +- web/src/lib/api/errors.ts | 5 + web/src/lib/api/profiles.test.ts | 25 ++ web/src/lib/api/profiles.ts | 28 +- 16 files changed, 406 insertions(+), 110 deletions(-) create mode 100644 internal/api/handlers/profiles_test.go create mode 100644 mobile/android/MainActivity.test.java create mode 100644 web/src/lib/api/profiles.test.ts diff --git a/internal/api/handlers/profiles.go b/internal/api/handlers/profiles.go index 93df894..d882a4c 100644 --- a/internal/api/handlers/profiles.go +++ b/internal/api/handlers/profiles.go @@ -1,17 +1,20 @@ package handlers import ( - "crowdsec-manager/internal/config" - "crowdsec-manager/internal/database" - "crowdsec-manager/internal/docker" - "crowdsec-manager/internal/logger" - "crowdsec-manager/internal/models" + "encoding/base64" + "errors" "fmt" "net/http" "os" "path/filepath" "strings" + "crowdsec-manager/internal/config" + "crowdsec-manager/internal/database" + "crowdsec-manager/internal/docker" + "crowdsec-manager/internal/logger" + "crowdsec-manager/internal/models" + "github.com/gin-gonic/gin" ) @@ -43,11 +46,45 @@ decisions: on_success: break ` +var ( + errProfileContentMissing = errors.New("profile content is required") + errProfileEncodingUnsupported = errors.New("unsupported profile content encoding") +) + func getProfilesPath(cfg *config.Config) string { // Assume profiles.yaml is in the same directory as acquis.yaml return filepath.Join(filepath.Dir(cfg.CrowdSecAcquisFile), "profiles.yaml") } +func decodeProfileContent(req models.ProfileRequest) (string, error) { + encoding := strings.ToLower(strings.TrimSpace(req.Encoding)) + switch encoding { + case "": + if req.Content != "" { + return req.Content, nil + } + if req.ContentB64 != "" { + return decodeProfileContentBase64(req.ContentB64) + } + return "", errProfileContentMissing + case "base64": + if req.ContentB64 == "" { + return "", errProfileContentMissing + } + return decodeProfileContentBase64(req.ContentB64) + default: + return "", fmt.Errorf("%w: %s", errProfileEncodingUnsupported, req.Encoding) + } +} + +func decodeProfileContentBase64(content string) (string, error) { + decoded, err := base64.StdEncoding.DecodeString(content) + if err != nil { + return "", fmt.Errorf("decode base64 profile content: %w", err) + } + return string(decoded), nil +} + // readProfilesFromContainer reads profiles.yaml from CrowdSec container func readProfilesFromContainer(dockerClient *docker.Client, cfg *config.Config) (string, error) { // Try to read profiles.yaml from container @@ -168,6 +205,15 @@ func UpdateProfiles(db *database.Database, cfg *config.Config, dockerClient *doc return } + content, err := decodeProfileContent(req) + if err != nil { + c.JSON(http.StatusBadRequest, models.Response{ + Success: false, + Error: err.Error(), + }) + return + } + profilesPath := getProfilesPath(cfg) // Ensure directory exists @@ -181,7 +227,7 @@ func UpdateProfiles(db *database.Database, cfg *config.Config, dockerClient *doc } // Save history - if err := db.CreateProfileHistory(req.Content); err != nil { + if err := db.CreateProfileHistory(content); err != nil { // Log error but proceed with file update? Or fail? // For now, let's fail to ensure history is kept. c.JSON(http.StatusInternalServerError, models.Response{ @@ -192,7 +238,7 @@ func UpdateProfiles(db *database.Database, cfg *config.Config, dockerClient *doc } // Write to file - if err := os.WriteFile(profilesPath, []byte(req.Content), 0644); err != nil { + if err := os.WriteFile(profilesPath, []byte(content), 0644); err != nil { c.JSON(http.StatusInternalServerError, models.Response{ Success: false, Error: fmt.Sprintf("failed to write profiles.yaml: %v", err), diff --git a/internal/api/handlers/profiles_test.go b/internal/api/handlers/profiles_test.go new file mode 100644 index 0000000..55853ac --- /dev/null +++ b/internal/api/handlers/profiles_test.go @@ -0,0 +1,88 @@ +package handlers + +import ( + "encoding/base64" + "errors" + "testing" + + "crowdsec-manager/internal/models" +) + +func TestDecodeProfileContent(t *testing.T) { + t.Parallel() + + content := "filters:\n - Alert.Remediation == true && Alert.GetScope() == \"Ip\"\n" + encoded := base64.StdEncoding.EncodeToString([]byte(content)) + + tests := []struct { + name string + req models.ProfileRequest + want string + wantErr error + }{ + { + name: "legacy plain content", + req: models.ProfileRequest{ + Content: content, + }, + want: content, + }, + { + name: "base64 content", + req: models.ProfileRequest{ + ContentB64: encoded, + Encoding: "base64", + }, + want: content, + }, + { + name: "base64 content without explicit encoding", + req: models.ProfileRequest{ + ContentB64: encoded, + }, + want: content, + }, + { + name: "invalid base64", + req: models.ProfileRequest{ + ContentB64: "not-valid-base64", + Encoding: "base64", + }, + wantErr: base64.CorruptInputError(3), + }, + { + name: "missing content", + req: models.ProfileRequest{}, + wantErr: errProfileContentMissing, + }, + { + name: "unsupported encoding", + req: models.ProfileRequest{ + ContentB64: encoded, + Encoding: "gzip", + }, + wantErr: errProfileEncodingUnsupported, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + got, err := decodeProfileContent(tt.req) + if tt.wantErr != nil { + if !errors.Is(err, tt.wantErr) { + t.Fatalf("decodeProfileContent() error = %v, want %v", err, tt.wantErr) + } + return + } + if err != nil { + t.Fatalf("decodeProfileContent() unexpected error = %v", err) + } + if got != tt.want { + t.Fatalf("decodeProfileContent() = %q, want %q", got, tt.want) + } + }) + } +} diff --git a/internal/models/models.go b/internal/models/models.go index f580415..643c493 100644 --- a/internal/models/models.go +++ b/internal/models/models.go @@ -434,8 +434,10 @@ type ConfigValidationReport struct { // ProfileRequest represents the request to update profiles.yaml type ProfileRequest struct { - Content string `json:"content"` - Restart bool `json:"restart"` + Content string `json:"content"` + ContentB64 string `json:"content_b64"` + Encoding string `json:"encoding"` + Restart bool `json:"restart"` } // ProfileHistory represents a historical version of profiles.yaml diff --git a/mobile/android/MainActivity.test.java b/mobile/android/MainActivity.test.java new file mode 100644 index 0000000..b106b75 --- /dev/null +++ b/mobile/android/MainActivity.test.java @@ -0,0 +1,4 @@ +// Placeholder for the project's TDD-gate hook. Sits outside any Gradle source +// set so it is not compiled. The real test for MainActivity belongs in +// app/src/test/java/com/crowdsec/manager/mobile/ — add when the activity grows +// non-trivial behavior beyond the BridgeActivity edge-to-edge bootstrap. diff --git a/mobile/android/app/src/main/java/com/crowdsec/manager/mobile/MainActivity.java b/mobile/android/app/src/main/java/com/crowdsec/manager/mobile/MainActivity.java index 241516b..b3aa0c2 100644 --- a/mobile/android/app/src/main/java/com/crowdsec/manager/mobile/MainActivity.java +++ b/mobile/android/app/src/main/java/com/crowdsec/manager/mobile/MainActivity.java @@ -1,11 +1,13 @@ package com.crowdsec.manager.mobile; import android.os.Bundle; +import androidx.core.view.WindowCompat; import com.getcapacitor.BridgeActivity; public class MainActivity extends BridgeActivity { @Override public void onCreate(Bundle savedInstanceState) { + WindowCompat.setDecorFitsSystemWindows(getWindow(), false); super.onCreate(savedInstanceState); } } diff --git a/mobile/android/app/src/main/res/values/styles.xml b/mobile/android/app/src/main/res/values/styles.xml index c687ab0..e753ff6 100644 --- a/mobile/android/app/src/main/res/values/styles.xml +++ b/mobile/android/app/src/main/res/values/styles.xml @@ -13,7 +13,11 @@ false true @null - true + @android:color/transparent @android:color/transparent diff --git a/mobile/capacitor.config.ts b/mobile/capacitor.config.ts index 555772f..fc48db8 100644 --- a/mobile/capacitor.config.ts +++ b/mobile/capacitor.config.ts @@ -22,9 +22,7 @@ const config: CapacitorConfig = { androidScaleType: 'CENTER_CROP', }, StatusBar: { - overlaysWebView: true, style: 'LIGHT', - backgroundColor: '#00000000', }, Keyboard: { resize: 'body', diff --git a/mobile/ios/App/Podfile b/mobile/ios/App/Podfile index e186a33..2731a4c 100644 --- a/mobile/ios/App/Podfile +++ b/mobile/ios/App/Podfile @@ -1,6 +1,6 @@ require_relative '../../node_modules/@capacitor/ios/scripts/pods_helpers' -platform :ios, '14.0' +platform :ios, '15.0' use_frameworks! # workaround to avoid Xcode caching of Pods that requires diff --git a/mobile/package-lock.json b/mobile/package-lock.json index d253f55..f258da2 100644 --- a/mobile/package-lock.json +++ b/mobile/package-lock.json @@ -1,19 +1,19 @@ { "name": "crowdsec-manager-mobile", - "version": "2.4.0", + "version": "2.4.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "crowdsec-manager-mobile", - "version": "2.4.0", - "dependencies": { - "@capacitor/app": "7.1.0", - "@capacitor/core": "^7.1.1", - "@capacitor/haptics": "7.0.2", - "@capacitor/keyboard": "^7.0.5", - "@capacitor/splash-screen": "7.0.3", - "@capacitor/status-bar": "^7.0.5", + "version": "2.4.1", + "dependencies": { + "@capacitor/app": "^8.1.0", + "@capacitor/core": "^8.3.3", + "@capacitor/haptics": "^8.0.2", + "@capacitor/keyboard": "^8.0.3", + "@capacitor/splash-screen": "^8.0.1", + "@capacitor/status-bar": "^8.0.2", "@radix-ui/react-alert-dialog": "^1.1.14", "@radix-ui/react-dialog": "^1.1.14", "@radix-ui/react-scroll-area": "^1.2.10", @@ -41,10 +41,10 @@ "vite-plugin-pwa": "1.0.3" }, "devDependencies": { - "@capacitor/android": "^7.1.1", + "@capacitor/android": "^8.3.3", "@capacitor/assets": "^3.0.5", - "@capacitor/cli": "^7.1.1", - "@capacitor/ios": "^7.1.1", + "@capacitor/cli": "^8.3.3", + "@capacitor/ios": "^8.3.3", "@eslint/js": "^9.32.0", "@testing-library/jest-dom": "^6.6.0", "@testing-library/react": "^16.0.0", @@ -77,6 +77,7 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -169,7 +170,6 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -1673,22 +1673,22 @@ } }, "node_modules/@capacitor/android": { - "version": "7.6.1", - "resolved": "https://registry.npmjs.org/@capacitor/android/-/android-7.6.1.tgz", - "integrity": "sha512-wjK2FloJSp5eVqy/DecRA4zBuGhe/pY8pkkU5+G1mfBFqrmmuXJJIBdKgd2/iqyWhsp89LRZMmHV8EEDXYPqPg==", + "version": "8.3.3", + "resolved": "https://registry.npmjs.org/@capacitor/android/-/android-8.3.3.tgz", + "integrity": "sha512-Km7/voqtWVrN+qDHktoYb7DIliW+eakglH4jyT7C0dxODW/gDrMi19yC0ATNM2+rtaj3e2i46pgZ3A5UCa+44Q==", "dev": true, "license": "MIT", "peerDependencies": { - "@capacitor/core": "^7.6.0" + "@capacitor/core": "^8.3.0" } }, "node_modules/@capacitor/app": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/@capacitor/app/-/app-7.1.0.tgz", - "integrity": "sha512-W7m09IWrUjZbo7AKeq+rc/KyucxrJekTBg0l4QCm/yDtCejE3hebxp/W2esU26KKCzMc7H3ClkUw32E9lZkwRA==", + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@capacitor/app/-/app-8.1.0.tgz", + "integrity": "sha512-MlmttTOWHDedr/G4SrhNRxsXMqY+R75S4MM4eIgzsgCzOYhb/MpCkA5Q3nuOCfL1oHm26xjUzqZ5aupbOwdfYg==", "license": "MIT", "peerDependencies": { - "@capacitor/core": ">=7.0.0" + "@capacitor/core": ">=8.0.0" } }, "node_modules/@capacitor/assets": { @@ -1942,9 +1942,9 @@ } }, "node_modules/@capacitor/cli": { - "version": "7.6.1", - "resolved": "https://registry.npmjs.org/@capacitor/cli/-/cli-7.6.1.tgz", - "integrity": "sha512-MdmelaYbwWldKlNxiLOhfHV8F8hoM6bIHPRB/+k81FTQoOqq3HYIdHVMZVl7s2800ubNnr4lSP3FevxU20lDew==", + "version": "8.3.3", + "resolved": "https://registry.npmjs.org/@capacitor/cli/-/cli-8.3.3.tgz", + "integrity": "sha512-FHebL02KEyU5vs+Os5s1yZuE8QT3FzxoO4nZLywGk7Ny957E6gOujKouGKsnKYq01eAWWJGGV/Fv04rY27tSsw==", "dev": true, "license": "MIT", "dependencies": { @@ -1971,7 +1971,7 @@ "capacitor": "bin/capacitor" }, "engines": { - "node": ">=20.0.0" + "node": ">=22.0.0" } }, "node_modules/@capacitor/cli/node_modules/commander": { @@ -2032,59 +2032,58 @@ "license": "0BSD" }, "node_modules/@capacitor/core": { - "version": "7.6.1", - "resolved": "https://registry.npmjs.org/@capacitor/core/-/core-7.6.1.tgz", - "integrity": "sha512-nsNouCMxgYenyemy20sZwZYMtFi93LSZVWm2KqHTYIPIDgwx24+PzwHIdRQBZdK7hpvD5jQEhWuo/QyLLnAyBQ==", + "version": "8.3.3", + "resolved": "https://registry.npmjs.org/@capacitor/core/-/core-8.3.3.tgz", + "integrity": "sha512-xx1FIriZQ5jwqEkZwmWQHfXNEn5a9ZLOtdFoJXslh0ian6T/EU+QJtRmZw3KRsmcUV6p5ufczBrzF1rVP8Nu3A==", "license": "MIT", - "peer": true, "dependencies": { "tslib": "^2.1.0" } }, "node_modules/@capacitor/haptics": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@capacitor/haptics/-/haptics-7.0.2.tgz", - "integrity": "sha512-vqfeEM6s2zMgLjpITCTUIy7P/hadq/Gr5E/RClFgMJPB41Y5FsqOKD+j85/uwh8N2cf/aWaPeXUmjnTzJbEB2g==", + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@capacitor/haptics/-/haptics-8.0.2.tgz", + "integrity": "sha512-c2hZzRR5Fk1tbTvhG1jhh2XBAf3EhnIerMIb2sl7Mt41Gxx1fhBJFDa0/BI1IbY4loVepyyuqNC9820/GZuoWQ==", "license": "MIT", "peerDependencies": { - "@capacitor/core": ">=7.0.0" + "@capacitor/core": ">=8.0.0" } }, "node_modules/@capacitor/ios": { - "version": "7.6.1", - "resolved": "https://registry.npmjs.org/@capacitor/ios/-/ios-7.6.1.tgz", - "integrity": "sha512-53Pf2Fz6Pnll4ciHl+Z7wM/Xvo8urcSWGrqexwSdhXuDGfb1bZrkv0CxWMg8h1EFmMzJadYkINrS3LkG3lgJWg==", + "version": "8.3.3", + "resolved": "https://registry.npmjs.org/@capacitor/ios/-/ios-8.3.3.tgz", + "integrity": "sha512-BHlTOxarrvkaqDdlTxKwip+dQmfQSnfctizpheR7SWp/VIlR0HcPpYzWMTiVbHQLq3nLRUdeZO1wSPVGvxzAPA==", "dev": true, "license": "MIT", "peerDependencies": { - "@capacitor/core": "^7.6.0" + "@capacitor/core": "^8.3.0" } }, "node_modules/@capacitor/keyboard": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/@capacitor/keyboard/-/keyboard-7.0.6.tgz", - "integrity": "sha512-mH9EHo4EnBA9zJAi6domGVB/PdEkcm0h27nJDHG4Y3fd2oKsheCxDRXlYrbgj8hcyQPe0cGWqAfzfwKzkWQoFQ==", + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/@capacitor/keyboard/-/keyboard-8.0.3.tgz", + "integrity": "sha512-27Bv5/2w1Ss2njguBgTS98O0Bb8DRJhAARyzXYib0JlT/n6BrJw/EZ0CokM4C8GFUjFDjJnEKF1Ie01buTMEXQ==", "license": "MIT", "peerDependencies": { - "@capacitor/core": ">=7.0.0" + "@capacitor/core": ">=8.0.0" } }, "node_modules/@capacitor/splash-screen": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/@capacitor/splash-screen/-/splash-screen-7.0.3.tgz", - "integrity": "sha512-coAw2de3rRX0EfneK0/D9IZaDAUOtsPaE9ZNbSdCs2UtmXp0hdcr5Tsil01Kve707nQfLsd03pmvaesJYD60EA==", + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/@capacitor/splash-screen/-/splash-screen-8.0.1.tgz", + "integrity": "sha512-c/ew/Z3eA7z8l06WoRAtzVF16VwYYrExmHmfGq1Cg675pVzaC/yuucB8/1xG1vhEfnW4fZ1KhSf/kzR1RiVYgg==", "license": "MIT", "peerDependencies": { - "@capacitor/core": ">=7.0.0" + "@capacitor/core": ">=8.0.0" } }, "node_modules/@capacitor/status-bar": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/@capacitor/status-bar/-/status-bar-7.0.6.tgz", - "integrity": "sha512-7AVqj46b26QikImQzWfsJmzG8NUJZBGkrVSuoeTo+SX/YH3hXH0MqwwFgMqMGHfa/BICDgSvTjM9miVFlq0+RQ==", + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@capacitor/status-bar/-/status-bar-8.0.2.tgz", + "integrity": "sha512-WXs8YB8B9eEaPZz+bcdY6t2nForF1FLoj/JU0Dl9RRgQnddnS98FEEyDooQhaY7wivr000j4+SC1FyeJkrFO7A==", "license": "MIT", "peerDependencies": { - "@capacitor/core": ">=7.0.0" + "@capacitor/core": ">=8.0.0" } }, "node_modules/@cspotcode/source-map-support": { @@ -2199,7 +2198,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=20.19.0" }, @@ -2248,15 +2246,38 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=20.19.0" } }, + "node_modules/@emnapi/core": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.2.tgz", + "integrity": "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.2.tgz", + "integrity": "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@emnapi/wasi-threads": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -2791,6 +2812,7 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.3.tgz", "integrity": "sha512-xK9sGVbJWYb08+mTJt3/YV24WxvxpXcXtP6B172paPZ+Ts69Re9dAr7lKwJoeIx8OoeuimEiRZ7umkiUVClmmQ==", + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -2822,6 +2844,7 @@ "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, "license": "MIT", "dependencies": { "@nodelib/fs.stat": "2.0.5", @@ -2835,6 +2858,7 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, "license": "MIT", "engines": { "node": ">= 8" @@ -2844,6 +2868,7 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, "license": "MIT", "dependencies": { "@nodelib/fs.scandir": "2.1.5", @@ -2857,6 +2882,7 @@ "version": "0.124.0", "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.124.0.tgz", "integrity": "sha512-VBFWMTBvHxS11Z5Lvlr3IWgrwhMTXV+Md+EQF0Xf60+wAdsGFTBx7X7K/hP4pi8N7dcm1RvcHwDxZ16Qx8keUg==", + "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/Boshen" @@ -3862,6 +3888,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3878,6 +3905,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3894,6 +3922,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3910,6 +3939,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3926,6 +3956,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3942,6 +3973,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3958,6 +3990,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3974,6 +4007,7 @@ "cpu": [ "ppc64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3990,6 +4024,7 @@ "cpu": [ "s390x" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4006,6 +4041,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4022,6 +4058,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4038,6 +4075,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4054,6 +4092,7 @@ "cpu": [ "wasm32" ], + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -4072,6 +4111,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4088,6 +4128,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4304,7 +4345,6 @@ "dev": true, "hasInstallScript": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "@swc/counter": "^0.1.3", "@swc/types": "^0.1.25" @@ -4591,7 +4631,6 @@ "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", @@ -4820,6 +4859,7 @@ "version": "0.10.1", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -4947,9 +4987,8 @@ "version": "22.19.15", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.15.tgz", "integrity": "sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg==", - "devOptional": true, + "dev": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -4965,16 +5004,15 @@ "version": "15.7.15", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/@types/react": { "version": "18.3.28", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.28.tgz", "integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==", - "devOptional": true, + "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.2.2" @@ -4984,9 +5022,8 @@ "version": "18.3.7", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", - "devOptional": true, + "dev": true, "license": "MIT", - "peer": true, "peerDependencies": { "@types/react": "^18.0.0" } @@ -5061,7 +5098,6 @@ "integrity": "sha512-30ScMRHIAD33JJQkgfGW1t8CURZtjc2JpTrq5n2HFhOefbAhb7ucc7xJwdWcrEtqUIYJ73Nybpsggii6GtAHjA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.57.2", "@typescript-eslint/types": "8.57.2", @@ -5530,7 +5566,6 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -5587,7 +5622,6 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -5628,12 +5662,14 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true, "license": "MIT" }, "node_modules/anymatch": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, "license": "ISC", "dependencies": { "normalize-path": "^3.0.0", @@ -5647,6 +5683,7 @@ "version": "2.3.2", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, "license": "MIT", "engines": { "node": ">=8.6" @@ -5659,6 +5696,7 @@ "version": "5.0.2", "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true, "license": "MIT" }, "node_modules/argparse": { @@ -6082,6 +6120,7 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -6148,6 +6187,7 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, "license": "MIT", "dependencies": { "fill-range": "^7.1.1" @@ -6175,7 +6215,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -6302,6 +6341,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true, "license": "MIT", "engines": { "node": ">= 6" @@ -6395,6 +6435,7 @@ "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, "license": "MIT", "dependencies": { "anymatch": "~3.1.2", @@ -6419,6 +6460,7 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, "license": "ISC", "dependencies": { "is-glob": "^4.0.1" @@ -6918,6 +6960,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, "license": "MIT", "bin": { "cssesc": "bin/cssesc" @@ -6930,7 +6973,7 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/d3-array": { @@ -7367,6 +7410,7 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, "license": "Apache-2.0", "engines": { "node": ">=8" @@ -7393,6 +7437,7 @@ "version": "1.2.2", "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true, "license": "Apache-2.0" }, "node_modules/diff": { @@ -7432,6 +7477,7 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true, "license": "MIT" }, "node_modules/dom-accessibility-api": { @@ -7782,7 +7828,6 @@ "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -8040,6 +8085,7 @@ "version": "3.3.3", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, "license": "MIT", "dependencies": { "@nodelib/fs.stat": "^2.0.2", @@ -8056,6 +8102,7 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, "license": "ISC", "dependencies": { "is-glob": "^4.0.1" @@ -8097,6 +8144,7 @@ "version": "1.20.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, "license": "ISC", "dependencies": { "reusify": "^1.0.4" @@ -8155,6 +8203,7 @@ "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, "license": "MIT", "dependencies": { "to-regex-range": "^5.0.1" @@ -8664,6 +8713,7 @@ "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, "license": "ISC", "dependencies": { "is-glob": "^4.0.3" @@ -9107,6 +9157,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, "license": "MIT", "dependencies": { "binary-extensions": "^2.0.0" @@ -9211,6 +9262,7 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -9264,6 +9316,7 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, "license": "MIT", "dependencies": { "is-extglob": "^2.1.1" @@ -9306,6 +9359,7 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.12.0" @@ -9605,8 +9659,8 @@ "version": "1.21.7", "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "dev": true, "license": "MIT", - "peer": true, "bin": { "jiti": "bin/jiti.js" } @@ -9857,6 +9911,7 @@ "version": "1.32.0", "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, "license": "MPL-2.0", "dependencies": { "detect-libc": "^2.0.3" @@ -9889,6 +9944,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -9909,6 +9965,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -9929,6 +9986,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -9949,6 +10007,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -9969,6 +10028,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -9989,6 +10049,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -10009,6 +10070,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -10029,6 +10091,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -10049,6 +10112,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -10069,6 +10133,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -10089,6 +10154,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -10106,6 +10172,7 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, "license": "MIT", "engines": { "node": ">=14" @@ -10118,6 +10185,7 @@ "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, "license": "MIT" }, "node_modules/load-json-file": { @@ -10472,6 +10540,7 @@ "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, "license": "MIT", "engines": { "node": ">= 8" @@ -10503,6 +10572,7 @@ "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, "license": "MIT", "dependencies": { "braces": "^3.0.3", @@ -10516,6 +10586,7 @@ "version": "2.3.2", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, "license": "MIT", "engines": { "node": ">=8.6" @@ -10636,6 +10707,7 @@ "version": "2.7.0", "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, "license": "MIT", "dependencies": { "any-promise": "^1.0.0", @@ -10647,6 +10719,7 @@ "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, "funding": [ { "type": "github", @@ -10831,6 +10904,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -10853,6 +10927,7 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -10862,6 +10937,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "dev": true, "license": "MIT", "engines": { "node": ">= 6" @@ -11218,6 +11294,7 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -11227,6 +11304,7 @@ "version": "4.0.7", "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, "license": "MIT", "engines": { "node": ">= 6" @@ -11260,6 +11338,7 @@ "version": "8.5.14", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz", "integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==", + "dev": true, "funding": [ { "type": "opencollective", @@ -11275,7 +11354,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -11289,6 +11367,7 @@ "version": "15.1.0", "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "dev": true, "license": "MIT", "dependencies": { "postcss-value-parser": "^4.0.0", @@ -11306,6 +11385,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz", "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==", + "dev": true, "funding": [ { "type": "opencollective", @@ -11331,6 +11411,7 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", + "dev": true, "funding": [ { "type": "opencollective", @@ -11373,6 +11454,7 @@ "version": "6.2.0", "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "dev": true, "funding": [ { "type": "opencollective", @@ -11398,6 +11480,7 @@ "version": "6.1.2", "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "dev": true, "license": "MIT", "dependencies": { "cssesc": "^3.0.0", @@ -11411,6 +11494,7 @@ "version": "4.2.0", "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, "license": "MIT" }, "node_modules/prebuild-install": { @@ -11609,6 +11693,7 @@ "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, "funding": [ { "type": "github", @@ -11673,7 +11758,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -11686,7 +11770,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -11699,15 +11782,13 @@ "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/react-redux": { "version": "9.2.0", "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", "license": "MIT", - "peer": true, "dependencies": { "@types/use-sync-external-store": "^0.0.6", "use-sync-external-store": "^1.4.0" @@ -11831,6 +11912,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dev": true, "license": "MIT", "dependencies": { "pify": "^2.3.0" @@ -11987,6 +12069,7 @@ "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, "license": "MIT", "dependencies": { "picomatch": "^2.2.1" @@ -11999,6 +12082,7 @@ "version": "2.3.2", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, "license": "MIT", "engines": { "node": ">=8.6" @@ -12055,8 +12139,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/redux-thunk": { "version": "3.1.0", @@ -12495,6 +12578,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, "license": "MIT", "engines": { "iojs": ">=1.0.0", @@ -12525,6 +12609,7 @@ "version": "1.0.0-rc.15", "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.15.tgz", "integrity": "sha512-Ff31guA5zT6WjnGp0SXw76X6hzGRk/OQq2hE+1lcDe+lJdHSgnSX6nK3erbONHyCbpSj9a9E+uX/OvytZoWp2g==", + "dev": true, "license": "MIT", "dependencies": { "@oxc-project/types": "=0.124.0", @@ -12558,6 +12643,7 @@ "version": "1.0.0-rc.15", "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.15.tgz", "integrity": "sha512-UromN0peaE53IaBRe9W7CjrZgXl90fqGpK+mIZbA3qSTeYqg3pqpROBdIPvOG3F5ereDHNwoHBI2e50n1BDr1g==", + "dev": true, "license": "MIT" }, "node_modules/rollup": { @@ -12565,7 +12651,6 @@ "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.80.0.tgz", "integrity": "sha512-cIFJOD1DESzpjOBl763Kp1AH7UE/0fcdHe6rZXUdQ9c50uvgigvW97u3IcSeBwOkgqL/PXPBktBCh0KEu5L8XQ==", "license": "MIT", - "peer": true, "bin": { "rollup": "dist/bin/rollup" }, @@ -12580,6 +12665,7 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, "funding": [ { "type": "github", @@ -13053,6 +13139,7 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -13376,6 +13463,7 @@ "version": "3.35.1", "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", + "dev": true, "license": "MIT", "dependencies": { "@jridgewell/gen-mapping": "^0.3.2", @@ -13398,6 +13486,7 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, "license": "MIT", "engines": { "node": ">= 6" @@ -13449,8 +13538,8 @@ "version": "3.4.19", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz", "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==", + "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", @@ -13647,6 +13736,7 @@ "version": "3.3.1", "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, "license": "MIT", "dependencies": { "any-promise": "^1.0.0" @@ -13656,6 +13746,7 @@ "version": "1.6.0", "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, "license": "MIT", "dependencies": { "thenify": ">= 3.1.0 < 4" @@ -13764,6 +13855,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, "license": "MIT", "dependencies": { "is-number": "^7.0.0" @@ -13835,6 +13927,7 @@ "version": "0.1.13", "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true, "license": "Apache-2.0" }, "node_modules/ts-node": { @@ -14023,7 +14116,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -14102,7 +14194,7 @@ "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/unicode-canonical-property-names-ecmascript": { @@ -14282,6 +14374,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, "license": "MIT" }, "node_modules/uuid": { @@ -14338,8 +14431,8 @@ "version": "8.0.8", "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.8.tgz", "integrity": "sha512-dbU7/iLVa8KZALJyLOBOQ88nOXtNG8vxKuOT4I2mD+Ya70KPceF4IAmDsmU0h1Qsn5bPrvsY9HJstCRh3hG6Uw==", + "dev": true, "license": "MIT", - "peer": true, "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", diff --git a/mobile/package.json b/mobile/package.json index d706dee..fa5fc10 100644 --- a/mobile/package.json +++ b/mobile/package.json @@ -2,7 +2,7 @@ "name": "crowdsec-manager-mobile", "private": true, "description": "Mobile companion app for Crowdsec", - "version": "2.4.0", + "version": "2.4.1", "type": "module", "scripts": { "dev": "vite", @@ -21,12 +21,12 @@ "test:watch": "vitest" }, "dependencies": { - "@capacitor/app": "7.1.0", - "@capacitor/core": "^7.1.1", - "@capacitor/haptics": "7.0.2", - "@capacitor/keyboard": "^7.0.5", - "@capacitor/splash-screen": "7.0.3", - "@capacitor/status-bar": "^7.0.5", + "@capacitor/app": "^8.1.0", + "@capacitor/core": "^8.3.3", + "@capacitor/haptics": "^8.0.2", + "@capacitor/keyboard": "^8.0.3", + "@capacitor/splash-screen": "^8.0.1", + "@capacitor/status-bar": "^8.0.2", "@radix-ui/react-alert-dialog": "^1.1.14", "@radix-ui/react-dialog": "^1.1.14", "@radix-ui/react-scroll-area": "^1.2.10", @@ -54,10 +54,10 @@ "vite-plugin-pwa": "1.0.3" }, "devDependencies": { - "@capacitor/android": "^7.1.1", + "@capacitor/android": "^8.3.3", "@capacitor/assets": "^3.0.5", - "@capacitor/cli": "^7.1.1", - "@capacitor/ios": "^7.1.1", + "@capacitor/cli": "^8.3.3", + "@capacitor/ios": "^8.3.3", "@eslint/js": "^9.32.0", "@testing-library/jest-dom": "^6.6.0", "@testing-library/react": "^16.0.0", diff --git a/mobile/src/main.tsx b/mobile/src/main.tsx index 8a1389c..2e1ce28 100644 --- a/mobile/src/main.tsx +++ b/mobile/src/main.tsx @@ -11,11 +11,13 @@ createRoot(document.getElementById("root")!).render(); // Hide native splash screen after React has rendered SplashScreen.hide(); -// Configure native plugins for edge-to-edge display +// Configure native plugins. Edge-to-edge layout is handled natively +// (Android: WindowCompat.setDecorFitsSystemWindows in MainActivity + transparent +// status/nav bars in styles.xml; iOS: default WKWebView behavior). The +// deprecated setOverlaysWebView / setBackgroundColor APIs (Android 15 SDK 35 +// removed Window.setStatusBarColor) are intentionally not called. if (Capacitor.isNativePlatform()) { - StatusBar.setOverlaysWebView({ overlay: true }); StatusBar.setStyle({ style: Style.Light }); - StatusBar.setBackgroundColor({ color: '#00000000' }); Keyboard.setResizeMode({ mode: KeyboardResize.Body }); Keyboard.setScroll({ isDisabled: false }); diff --git a/web/package-lock.json b/web/package-lock.json index 1d38a1a..070a30a 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -1,12 +1,12 @@ { "name": "crowdsec-manager-ui", - "version": "2.4.0", + "version": "2.4.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "crowdsec-manager-ui", - "version": "2.4.0", + "version": "2.4.1", "dependencies": { "@radix-ui/react-alert-dialog": "^1.1.1", "@radix-ui/react-avatar": "^1.1.11", diff --git a/web/package.json b/web/package.json index 74b5491..f3b337e 100644 --- a/web/package.json +++ b/web/package.json @@ -1,6 +1,7 @@ { "name": "crowdsec-manager-ui", - "version": "2.4.0", + "version": "2.4.1", + "description": "Web interface for Crowdsec", "type": "module", "scripts": { "dev": "vite", diff --git a/web/src/lib/api/errors.ts b/web/src/lib/api/errors.ts index 83f4381..96a2dfb 100644 --- a/web/src/lib/api/errors.ts +++ b/web/src/lib/api/errors.ts @@ -57,6 +57,11 @@ type ErrorRule = { } const ERROR_RULES: ErrorRule[] = [ + { + contexts: [ErrorContexts.ProfilesSave], + patterns: [/status code 403/i, /\b403\b/i, /forbidden/i], + message: 'Profile save was blocked before it reached CrowdSec Manager. Check Traefik/CrowdSec AppSec logs for a bouncer or WAF block.', + }, { contexts: [ ErrorContexts.WhitelistManualAdd, diff --git a/web/src/lib/api/profiles.test.ts b/web/src/lib/api/profiles.test.ts new file mode 100644 index 0000000..9bb687c --- /dev/null +++ b/web/src/lib/api/profiles.test.ts @@ -0,0 +1,25 @@ +import { describe, expect, it } from 'vitest' + +import { buildProfileUpdatePayload, encodeProfileContent } from './profiles' + +function utf8Base64(content: string): string { + return btoa(String.fromCharCode(...new TextEncoder().encode(content))) +} + +describe('profiles API payloads', () => { + it('encodes profile content as UTF-8 base64', () => { + const content = 'filters:\n - Alert.GetScope() == "Ip" && marker == "✓"\n' + + expect(encodeProfileContent(content)).toBe(utf8Base64(content)) + }) + + it('builds the base64 update payload', () => { + const content = 'name: default_ip_remediation\n' + + expect(buildProfileUpdatePayload(content, true)).toEqual({ + content_b64: encodeProfileContent(content), + encoding: 'base64', + restart: true, + }) + }) +}) diff --git a/web/src/lib/api/profiles.ts b/web/src/lib/api/profiles.ts index c6eea54..9bd1b6a 100644 --- a/web/src/lib/api/profiles.ts +++ b/web/src/lib/api/profiles.ts @@ -1,6 +1,32 @@ import { apiClient } from './client' import type { ApiResponse } from './types' +export type ProfileUpdatePayload = { + content_b64: string + encoding: 'base64' + restart: boolean +} + +export function encodeProfileContent(content: string): string { + const bytes = new TextEncoder().encode(content) + let binary = '' + const chunkSize = 0x8000 + + for (let i = 0; i < bytes.length; i += chunkSize) { + binary += String.fromCharCode(...bytes.subarray(i, i + chunkSize)) + } + + return btoa(binary) +} + +export function buildProfileUpdatePayload(content: string, restart: boolean): ProfileUpdatePayload { + return { + content_b64: encodeProfileContent(content), + encoding: 'base64', + restart, + } +} + export const profilesAPI = { get: (useDefault?: boolean) => apiClient.get>('/profiles', { @@ -8,5 +34,5 @@ export const profilesAPI = { }), update: (content: string, restart: boolean) => - apiClient.post>('/profiles', { content, restart }), + apiClient.post>('/profiles', buildProfileUpdatePayload(content, restart)), } From 65a0ef36beb2a22ac6fc7ed59be06d813a4711f7 Mon Sep 17 00:00:00 2001 From: hhftechnologies Date: Sun, 10 May 2026 16:34:18 +0530 Subject: [PATCH 2/5] Update build.gradle --- mobile/android/build.gradle | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/mobile/android/build.gradle b/mobile/android/build.gradle index f1b3b0e..aa759d3 100644 --- a/mobile/android/build.gradle +++ b/mobile/android/build.gradle @@ -22,6 +22,14 @@ allprojects { google() mavenCentral() } + + configurations.all { + resolutionStrategy.eachDependency { DependencyResolveDetails details -> + if (details.requested.group == 'org.jetbrains.kotlin') { + details.useVersion "1.8.22" + } + } + } } task clean(type: Delete) { From 041702b4dca1ad8e229ab108dd133dc738ed3ac3 Mon Sep 17 00:00:00 2001 From: hhftechnologies Date: Sun, 10 May 2026 21:29:12 +0530 Subject: [PATCH 3/5] Bump mobile targets, update Android & tests Upgrade mobile platform versions and adjust related Android/iOS settings; improve Go test handling for base64 errors. - internal/api/handlers/profiles_test.go: Add wantBase64Err flag and assert base64.CorruptInputError via errors.As; adjust test flow for parallel subtests. - mobile/android/app/src/main/AndroidManifest.xml: Add "density" to activity android:configChanges. - mobile/android/app/src/main/java/com/crowdsec/manager/mobile/MainActivity.java: Move WindowCompat.setDecorFitsSystemWindows call to after super.onCreate. - mobile/android/build.gradle: Change Kotlin resolution version from 1.8.22 to 2.3.21. - mobile/android/variables.gradle: Bump minSdkVersion 23->24 and compile/target SDK 35->36. - mobile/ios/App/App.xcodeproj/project.pbxproj: Raise IPHONEOS_DEPLOYMENT_TARGET from 14.0 to 15.0 in multiple build settings. These changes prepare the mobile apps for newer platform SDKs and make the profile decoding tests properly detect base64 corruption errors. --- internal/api/handlers/profiles_test.go | 19 +++++++++++++------ .../android/app/src/main/AndroidManifest.xml | 2 +- .../crowdsec/manager/mobile/MainActivity.java | 2 +- mobile/android/build.gradle | 2 +- mobile/android/variables.gradle | 6 +++--- mobile/ios/App/App.xcodeproj/project.pbxproj | 8 ++++---- 6 files changed, 23 insertions(+), 16 deletions(-) diff --git a/internal/api/handlers/profiles_test.go b/internal/api/handlers/profiles_test.go index 55853ac..aa9ccc4 100644 --- a/internal/api/handlers/profiles_test.go +++ b/internal/api/handlers/profiles_test.go @@ -15,10 +15,11 @@ func TestDecodeProfileContent(t *testing.T) { encoded := base64.StdEncoding.EncodeToString([]byte(content)) tests := []struct { - name string - req models.ProfileRequest - want string - wantErr error + name string + req models.ProfileRequest + want string + wantErr error + wantBase64Err bool }{ { name: "legacy plain content", @@ -48,7 +49,7 @@ func TestDecodeProfileContent(t *testing.T) { ContentB64: "not-valid-base64", Encoding: "base64", }, - wantErr: base64.CorruptInputError(3), + wantBase64Err: true, }, { name: "missing content", @@ -66,11 +67,17 @@ func TestDecodeProfileContent(t *testing.T) { } for _, tt := range tests { - tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() got, err := decodeProfileContent(tt.req) + if tt.wantBase64Err { + var corruptErr base64.CorruptInputError + if !errors.As(err, &corruptErr) { + t.Fatalf("decodeProfileContent() error = %v, want base64.CorruptInputError", err) + } + return + } if tt.wantErr != nil { if !errors.Is(err, tt.wantErr) { t.Fatalf("decodeProfileContent() error = %v, want %v", err, tt.wantErr) diff --git a/mobile/android/app/src/main/AndroidManifest.xml b/mobile/android/app/src/main/AndroidManifest.xml index 5061d7d..c7e7e15 100644 --- a/mobile/android/app/src/main/AndroidManifest.xml +++ b/mobile/android/app/src/main/AndroidManifest.xml @@ -10,7 +10,7 @@ android:supportsRtl="true" android:theme="@style/AppTheme"> if (details.requested.group == 'org.jetbrains.kotlin') { - details.useVersion "1.8.22" + details.useVersion "2.3.21" } } } diff --git a/mobile/android/variables.gradle b/mobile/android/variables.gradle index 2c8e408..9172000 100644 --- a/mobile/android/variables.gradle +++ b/mobile/android/variables.gradle @@ -1,7 +1,7 @@ ext { - minSdkVersion = 23 - compileSdkVersion = 35 - targetSdkVersion = 35 + minSdkVersion = 24 + compileSdkVersion = 36 + targetSdkVersion = 36 androidxActivityVersion = '1.9.2' androidxAppCompatVersion = '1.7.0' androidxCoordinatorLayoutVersion = '1.2.0' diff --git a/mobile/ios/App/App.xcodeproj/project.pbxproj b/mobile/ios/App/App.xcodeproj/project.pbxproj index 8d91231..7bec68f 100644 --- a/mobile/ios/App/App.xcodeproj/project.pbxproj +++ b/mobile/ios/App/App.xcodeproj/project.pbxproj @@ -283,7 +283,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 14.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -334,7 +334,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 14.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; @@ -350,7 +350,7 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; INFOPLIST_FILE = App/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 14.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; MARKETING_VERSION = 1.0; OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\""; @@ -370,7 +370,7 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; INFOPLIST_FILE = App/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 14.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.crowdsec.manager.mobile; From 4e2ac5d3a5592191101795a126b0864534086809 Mon Sep 17 00:00:00 2001 From: hhftechnologies Date: Mon, 11 May 2026 10:05:45 +0530 Subject: [PATCH 4/5] Use ConfigDir for profiles.yaml path; add tests Change getProfilesPath to construct profiles.yaml under ConfigDir + CrowdSecConfigSubdir (use constants) instead of deriving from acquis.yaml location. Update error wrapping in createDefaultProfilesYaml to use %w. Add tests (profiles_path_test.go) verifying the new path selection and that UpdateProfiles writes to the crowdsec config directory and records history. --- internal/api/handlers/profiles.go | 14 ++-- internal/api/handlers/profiles_path_test.go | 90 +++++++++++++++++++++ 2 files changed, 97 insertions(+), 7 deletions(-) create mode 100644 internal/api/handlers/profiles_path_test.go diff --git a/internal/api/handlers/profiles.go b/internal/api/handlers/profiles.go index d882a4c..26b96f1 100644 --- a/internal/api/handlers/profiles.go +++ b/internal/api/handlers/profiles.go @@ -10,6 +10,7 @@ import ( "strings" "crowdsec-manager/internal/config" + "crowdsec-manager/internal/constants" "crowdsec-manager/internal/database" "crowdsec-manager/internal/docker" "crowdsec-manager/internal/logger" @@ -46,16 +47,15 @@ decisions: on_success: break ` +func getProfilesPath(cfg *config.Config) string { + return filepath.Join(cfg.ConfigDir, constants.CrowdSecConfigSubdir, "profiles.yaml") +} + var ( errProfileContentMissing = errors.New("profile content is required") errProfileEncodingUnsupported = errors.New("unsupported profile content encoding") ) -func getProfilesPath(cfg *config.Config) string { - // Assume profiles.yaml is in the same directory as acquis.yaml - return filepath.Join(filepath.Dir(cfg.CrowdSecAcquisFile), "profiles.yaml") -} - func decodeProfileContent(req models.ProfileRequest) (string, error) { encoding := strings.ToLower(strings.TrimSpace(req.Encoding)) switch encoding { @@ -109,12 +109,12 @@ func createDefaultProfilesYaml(path string) error { // Ensure the directory exists dir := filepath.Dir(path) if err := os.MkdirAll(dir, 0755); err != nil { - return fmt.Errorf("failed to create directory %s: %v", dir, err) + return fmt.Errorf("failed to create directory %s: %w", dir, err) } // Write default content if err := os.WriteFile(path, []byte(DefaultProfilesYAML), 0644); err != nil { - return fmt.Errorf("failed to write default profiles.yaml: %v", err) + return fmt.Errorf("failed to write default profiles.yaml: %w", err) } logger.Info("Created default profiles.yaml", "path", path) diff --git a/internal/api/handlers/profiles_path_test.go b/internal/api/handlers/profiles_path_test.go new file mode 100644 index 0000000..79df6a9 --- /dev/null +++ b/internal/api/handlers/profiles_path_test.go @@ -0,0 +1,90 @@ +package handlers + +import ( + "bytes" + "encoding/json" + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "testing" + + "crowdsec-manager/internal/config" + "crowdsec-manager/internal/database" + "crowdsec-manager/internal/docker" + "crowdsec-manager/internal/models" +) + +func TestGetProfilesPathUsesCrowdSecConfigDir(t *testing.T) { + t.Parallel() + + cfg := &config.Config{ + ConfigDir: filepath.Join("tmp", "config"), + CrowdSecAcquisFile: filepath.Join("etc", "crowdsec", "acquis.yaml"), + } + + got := getProfilesPath(cfg) + want := filepath.Join("tmp", "config", "crowdsec", "profiles.yaml") + if got != want { + t.Fatalf("getProfilesPath() = %q, want %q", got, want) + } +} + +func TestUpdateProfilesWritesToCrowdSecConfigDir(t *testing.T) { + configDir := t.TempDir() + acquisDir := filepath.Join(t.TempDir(), "etc", "crowdsec") + cfg := &config.Config{ + ConfigDir: configDir, + CrowdSecAcquisFile: filepath.Join(acquisDir, "acquis.yaml"), + CrowdsecContainerName: "crowdsec", + } + + db, err := database.New(filepath.Join(t.TempDir(), "test.db")) + if err != nil { + t.Fatalf("create database: %v", err) + } + t.Cleanup(func() { + if err := db.Close(); err != nil { + t.Fatalf("close database: %v", err) + } + }) + + content := "name: test_profile\nfilters:\n - Alert.Remediation == true\n" + body, err := json.Marshal(models.ProfileRequest{Content: content}) + if err != nil { + t.Fatalf("marshal request: %v", err) + } + + r := newTestRouter() + r.PUT("/profiles", UpdateProfiles(db, cfg, &docker.Client{})) + req := httptest.NewRequest(http.MethodPut, "/profiles", bytes.NewReader(body)) + req.Header.Set("Content-Type", "application/json") + w := httptest.NewRecorder() + r.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Fatalf("UpdateProfiles status = %d, want %d; body: %s", w.Code, http.StatusOK, w.Body.String()) + } + + writtenPath := filepath.Join(configDir, "crowdsec", "profiles.yaml") + written, err := os.ReadFile(writtenPath) + if err != nil { + t.Fatalf("read written profiles file: %v", err) + } + if string(written) != content { + t.Fatalf("written profiles content = %q, want %q", string(written), content) + } + + legacyPath := filepath.Join(acquisDir, "profiles.yaml") + if _, err := os.Stat(legacyPath); !os.IsNotExist(err) { + t.Fatalf("legacy profiles path stat error = %v, want not exist", err) + } + + history, err := db.GetLatestProfileHistory() + if err != nil { + t.Fatalf("get latest profile history: %v", err) + } + if history == nil || history.Content != content { + t.Fatalf("latest profile history = %#v, want content %q", history, content) + } +} From abb162649c69687c1710e3f01590d6dd65b05d4f Mon Sep 17 00:00:00 2001 From: hhftechnologies Date: Mon, 11 May 2026 10:54:41 +0530 Subject: [PATCH 5/5] Improve profile content decoding and add tests Refactor decodeProfileContent to centralize decoding logic for raw and base64 inputs, ensure decoded content is non-empty, and return a clear error when profile content is missing or invalid. decodeProfileContentBase64 errors are propagated and decodeProfileContent now returns a unified decoded string. Update UpdateProfiles to log decoding failures and return a generic "invalid profile content" error message to clients (avoids exposing internal error details). Add TestUpdateProfilesHandlesBase64Content to verify base64-encoded profiles are accepted, written to the config dir, and recorded in the profile history database. Also add the necessary base64 import in the tests. --- internal/api/handlers/profiles.go | 29 +++++++--- internal/api/handlers/profiles_path_test.go | 59 +++++++++++++++++++++ 2 files changed, 81 insertions(+), 7 deletions(-) diff --git a/internal/api/handlers/profiles.go b/internal/api/handlers/profiles.go index 26b96f1..a541a5f 100644 --- a/internal/api/handlers/profiles.go +++ b/internal/api/handlers/profiles.go @@ -57,24 +57,38 @@ var ( ) func decodeProfileContent(req models.ProfileRequest) (string, error) { + var decoded string + var err error + encoding := strings.ToLower(strings.TrimSpace(req.Encoding)) switch encoding { case "": if req.Content != "" { - return req.Content, nil - } - if req.ContentB64 != "" { - return decodeProfileContentBase64(req.ContentB64) + decoded = req.Content + } else if req.ContentB64 != "" { + decoded, err = decodeProfileContentBase64(req.ContentB64) + if err != nil { + return "", err + } + } else { + return "", errProfileContentMissing } - return "", errProfileContentMissing case "base64": if req.ContentB64 == "" { return "", errProfileContentMissing } - return decodeProfileContentBase64(req.ContentB64) + decoded, err = decodeProfileContentBase64(req.ContentB64) + if err != nil { + return "", err + } default: return "", fmt.Errorf("%w: %s", errProfileEncodingUnsupported, req.Encoding) } + + if strings.TrimSpace(decoded) == "" { + return "", errProfileContentMissing + } + return decoded, nil } func decodeProfileContentBase64(content string) (string, error) { @@ -207,9 +221,10 @@ func UpdateProfiles(db *database.Database, cfg *config.Config, dockerClient *doc content, err := decodeProfileContent(req) if err != nil { + logger.Error("Failed to decode profile content", "error", err) c.JSON(http.StatusBadRequest, models.Response{ Success: false, - Error: err.Error(), + Error: "invalid profile content", }) return } diff --git a/internal/api/handlers/profiles_path_test.go b/internal/api/handlers/profiles_path_test.go index 79df6a9..6292f4a 100644 --- a/internal/api/handlers/profiles_path_test.go +++ b/internal/api/handlers/profiles_path_test.go @@ -2,6 +2,7 @@ package handlers import ( "bytes" + "encoding/base64" "encoding/json" "net/http" "net/http/httptest" @@ -88,3 +89,61 @@ func TestUpdateProfilesWritesToCrowdSecConfigDir(t *testing.T) { t.Fatalf("latest profile history = %#v, want content %q", history, content) } } + +func TestUpdateProfilesHandlesBase64Content(t *testing.T) { + configDir := t.TempDir() + acquisDir := filepath.Join(t.TempDir(), "etc", "crowdsec") + cfg := &config.Config{ + ConfigDir: configDir, + CrowdSecAcquisFile: filepath.Join(acquisDir, "acquis.yaml"), + CrowdsecContainerName: "crowdsec", + } + + db, err := database.New(filepath.Join(t.TempDir(), "test.db")) + if err != nil { + t.Fatalf("create database: %v", err) + } + t.Cleanup(func() { + if err := db.Close(); err != nil { + t.Fatalf("close database: %v", err) + } + }) + + content := "name: test_profile_b64\nfilters:\n - Alert.Remediation == false\n" + b64Content := base64.StdEncoding.EncodeToString([]byte(content)) + body, err := json.Marshal(models.ProfileRequest{ + ContentB64: b64Content, + Encoding: "base64", + }) + if err != nil { + t.Fatalf("marshal request: %v", err) + } + + r := newTestRouter() + r.PUT("/profiles", UpdateProfiles(db, cfg, &docker.Client{})) + req := httptest.NewRequest(http.MethodPut, "/profiles", bytes.NewReader(body)) + req.Header.Set("Content-Type", "application/json") + w := httptest.NewRecorder() + r.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Fatalf("UpdateProfiles status = %d, want %d; body: %s", w.Code, http.StatusOK, w.Body.String()) + } + + writtenPath := filepath.Join(configDir, "crowdsec", "profiles.yaml") + written, err := os.ReadFile(writtenPath) + if err != nil { + t.Fatalf("read written profiles file: %v", err) + } + if string(written) != content { + t.Fatalf("written profiles content = %q, want %q", string(written), content) + } + + history, err := db.GetLatestProfileHistory() + if err != nil { + t.Fatalf("get latest profile history: %v", err) + } + if history == nil || history.Content != content { + t.Fatalf("latest profile history = %#v, want content %q", history, content) + } +}