From 07266f4dcb1bbe53a2f12c861a96063235d2ed47 Mon Sep 17 00:00:00 2001 From: Mohamad Date: Mon, 5 May 2025 13:25:23 +0200 Subject: [PATCH] init to use html/css/js --- .env | 2 + backend/.gitignore => .gitignore | 0 backend/Cargo.lock => Cargo.lock | 301 +- backend/Cargo.toml => Cargo.toml | 10 +- Dockerfile | 20 - README.md | 221 - backend/src/auth.rs | 36 - backend/src/db.rs | 123 - backend/src/handlers.rs | 240 -- backend/src/main.rs | 64 - backend/src/middleware.rs | 87 - backend/src/models.rs | 33 - docker-compose.yml | 9 - backend/form_data.db => form_data.db | Bin frontend/.gitignore | 23 - frontend/.npmrc | 1 - frontend/.prettierignore | 4 - frontend/.prettierrc | 15 - frontend/README.md | 38 - frontend/eslint.config.js | 34 - frontend/package-lock.json | 3652 ----------------- frontend/package.json | 37 - frontend/src/app.css | 188 - frontend/src/app.d.ts | 13 - frontend/src/app.html | 12 - frontend/src/lib/api.ts | 176 - frontend/src/lib/components/Dashboard.svelte | 32 - .../src/lib/components/FormBuilder.svelte | 44 - .../src/lib/components/FormRenderer.svelte | 35 - frontend/src/lib/components/Navbar.svelte | 11 - frontend/src/lib/components/Routes.svelte | 32 - frontend/src/lib/session.svelte.ts | 17 - frontend/src/lib/types.ts | 29 - frontend/src/routes/(auth)/+layout.svelte | 7 - frontend/src/routes/(auth)/+layout.ts | 8 - .../src/routes/(auth)/create/+page.svelte | 62 - .../src/routes/(auth)/form/[id]/+page.svelte | 61 - frontend/src/routes/(auth)/form/[id]/+page.ts | 5 - frontend/src/routes/(auth)/main/+page.svelte | 22 - frontend/src/routes/+layout.svelte | 7 - frontend/src/routes/+layout.ts | 1 - frontend/src/routes/+page.ts | 7 - frontend/src/routes/login/+page.svelte | 51 - frontend/src/routes/login/+page.ts | 8 - frontend/static/favicon.png | Bin 1571 -> 0 bytes frontend/svelte.config.js | 18 - frontend/tsconfig.json | 19 - frontend/vite.config.ts | 6 - repomix-output.xml | 1555 +++++++ src/auth.rs | 99 + src/db.rs | 302 ++ src/handlers.rs | 746 ++++ src/main.rs | 167 + src/models.rs | 139 + 54 files changed, 3171 insertions(+), 5658 deletions(-) create mode 100644 .env rename backend/.gitignore => .gitignore (100%) rename backend/Cargo.lock => Cargo.lock (94%) rename backend/Cargo.toml => Cargo.toml (61%) delete mode 100644 Dockerfile delete mode 100644 README.md delete mode 100644 backend/src/auth.rs delete mode 100644 backend/src/db.rs delete mode 100644 backend/src/handlers.rs delete mode 100644 backend/src/main.rs delete mode 100644 backend/src/middleware.rs delete mode 100644 backend/src/models.rs delete mode 100644 docker-compose.yml rename backend/form_data.db => form_data.db (100%) delete mode 100644 frontend/.gitignore delete mode 100644 frontend/.npmrc delete mode 100644 frontend/.prettierignore delete mode 100644 frontend/.prettierrc delete mode 100644 frontend/README.md delete mode 100644 frontend/eslint.config.js delete mode 100644 frontend/package-lock.json delete mode 100644 frontend/package.json delete mode 100644 frontend/src/app.css delete mode 100644 frontend/src/app.d.ts delete mode 100644 frontend/src/app.html delete mode 100644 frontend/src/lib/api.ts delete mode 100644 frontend/src/lib/components/Dashboard.svelte delete mode 100644 frontend/src/lib/components/FormBuilder.svelte delete mode 100644 frontend/src/lib/components/FormRenderer.svelte delete mode 100644 frontend/src/lib/components/Navbar.svelte delete mode 100644 frontend/src/lib/components/Routes.svelte delete mode 100644 frontend/src/lib/session.svelte.ts delete mode 100644 frontend/src/lib/types.ts delete mode 100644 frontend/src/routes/(auth)/+layout.svelte delete mode 100644 frontend/src/routes/(auth)/+layout.ts delete mode 100644 frontend/src/routes/(auth)/create/+page.svelte delete mode 100644 frontend/src/routes/(auth)/form/[id]/+page.svelte delete mode 100644 frontend/src/routes/(auth)/form/[id]/+page.ts delete mode 100644 frontend/src/routes/(auth)/main/+page.svelte delete mode 100644 frontend/src/routes/+layout.svelte delete mode 100644 frontend/src/routes/+layout.ts delete mode 100644 frontend/src/routes/+page.ts delete mode 100644 frontend/src/routes/login/+page.svelte delete mode 100644 frontend/src/routes/login/+page.ts delete mode 100644 frontend/static/favicon.png delete mode 100644 frontend/svelte.config.js delete mode 100644 frontend/tsconfig.json delete mode 100644 frontend/vite.config.ts create mode 100644 repomix-output.xml create mode 100644 src/auth.rs create mode 100644 src/db.rs create mode 100644 src/handlers.rs create mode 100644 src/main.rs create mode 100644 src/models.rs diff --git a/.env b/.env new file mode 100644 index 0000000..92401fd --- /dev/null +++ b/.env @@ -0,0 +1,2 @@ +INITIAL_ADMIN_USERNAME=admin +INITIAL_ADMIN_PASSWORD=admin \ No newline at end of file diff --git a/backend/.gitignore b/.gitignore similarity index 100% rename from backend/.gitignore rename to .gitignore diff --git a/backend/Cargo.lock b/Cargo.lock similarity index 94% rename from backend/Cargo.lock rename to Cargo.lock index 1c6405a..a9ff401 100644 --- a/backend/Cargo.lock +++ b/Cargo.lock @@ -281,6 +281,21 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "anyhow" version = "1.0.95" @@ -338,15 +353,6 @@ version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" -[[package]] -name = "blake2" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" -dependencies = [ - "digest", -] - [[package]] name = "block-buffer" version = "0.10.4" @@ -389,9 +395,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.16.0" +version = "3.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" +checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" [[package]] name = "byteorder" @@ -431,6 +437,21 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chrono" +version = "0.4.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-link", +] + [[package]] name = "cipher" version = "0.4.4" @@ -458,6 +479,12 @@ dependencies = [ "version_check", ] +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + [[package]] name = "cpufeatures" version = "0.2.16" @@ -516,7 +543,6 @@ checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", "crypto-common", - "subtle", ] [[package]] @@ -530,6 +556,12 @@ dependencies = [ "syn", ] +[[package]] +name = "dotenv" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f" + [[package]] name = "encoding_rs" version = "0.8.35" @@ -604,13 +636,16 @@ dependencies = [ "actix-web", "anyhow", "bcrypt", + "chrono", + "dotenv", "env_logger", "futures", "log", - "rand_core", + "regex", "rusqlite", "serde", "serde_json", + "url", "uuid", ] @@ -720,10 +755,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", - "js-sys", "libc", "wasi", - "wasm-bindgen", ] [[package]] @@ -817,6 +850,30 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" +[[package]] +name = "iana-time-zone" +version = "0.1.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "icu_collections" version = "1.5.0" @@ -1009,29 +1066,14 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.76" +version = "0.3.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6717b6b5b077764fb5966237269cb3c64edddde4b14ce42647430a78ced9e7b7" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" dependencies = [ "once_cell", "wasm-bindgen", ] -[[package]] -name = "jsonwebtoken" -version = "9.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9ae10193d25051e74945f1ea2d0b42e03cc3b890f7e4cc5faa44997d808193f" -dependencies = [ - "base64 0.21.7", - "js-sys", - "pem", - "ring", - "serde", - "serde_json", - "simple_asn1", -] - [[package]] name = "language-tags" version = "0.3.2" @@ -1137,31 +1179,12 @@ dependencies = [ "windows-sys", ] -[[package]] -name = "num-bigint" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" -dependencies = [ - "num-integer", - "num-traits", -] - [[package]] name = "num-conv" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" -[[package]] -name = "num-integer" -version = "0.1.46" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" -dependencies = [ - "num-traits", -] - [[package]] name = "num-traits" version = "0.2.19" @@ -1209,33 +1232,12 @@ dependencies = [ "windows-targets", ] -[[package]] -name = "password-hash" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" -dependencies = [ - "base64ct", - "rand_core", - "subtle", -] - [[package]] name = "paste" version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" -[[package]] -name = "pem" -version = "3.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e459365e590736a54c3fa561947c84837534b8e9af6fc5bf781307e82658fae" -dependencies = [ - "base64 0.22.1", - "serde", -] - [[package]] name = "percent-encoding" version = "2.3.1" @@ -1367,21 +1369,6 @@ version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" -[[package]] -name = "ring" -version = "0.17.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" -dependencies = [ - "cc", - "cfg-if", - "getrandom", - "libc", - "spin", - "untrusted", - "windows-sys", -] - [[package]] name = "rusqlite" version = "0.29.0" @@ -1389,6 +1376,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "549b9d036d571d42e6e85d1c1425e2ac83491075078ca9a15be021c56b1641f2" dependencies = [ "bitflags", + "chrono", "fallible-iterator", "fallible-streaming-iterator", "hashlink", @@ -1411,6 +1399,12 @@ dependencies = [ "semver", ] +[[package]] +name = "rustversion" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" + [[package]] name = "ryu" version = "1.0.18" @@ -1499,18 +1493,6 @@ dependencies = [ "libc", ] -[[package]] -name = "simple_asn1" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adc4e5204eb1910f40f9cfa375f6f05b68c3abac4b6fd879c8ff5e7ae8a0a085" -dependencies = [ - "num-bigint", - "num-traits", - "thiserror", - "time", -] - [[package]] name = "slab" version = "0.4.9" @@ -1536,24 +1518,12 @@ dependencies = [ "windows-sys", ] -[[package]] -name = "spin" -version = "0.9.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" - [[package]] name = "stable_deref_trait" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" -[[package]] -name = "subtle" -version = "2.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" - [[package]] name = "syn" version = "2.0.92" @@ -1585,26 +1555,6 @@ dependencies = [ "winapi-util", ] -[[package]] -name = "thiserror" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" -dependencies = [ - "thiserror-impl", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "time" version = "0.3.37" @@ -1714,12 +1664,6 @@ version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" -[[package]] -name = "untrusted" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" - [[package]] name = "url" version = "2.5.4" @@ -1778,20 +1722,21 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.99" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a474f6281d1d70c17ae7aa6a613c87fce69a127e2624002df63dcb39d6cf6396" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" dependencies = [ "cfg-if", "once_cell", + "rustversion", "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" -version = "0.2.99" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f89bb38646b4f81674e8f5c3fb81b562be1fd936d84320f3264486418519c79" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" dependencies = [ "bumpalo", "log", @@ -1803,9 +1748,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.99" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2cc6181fd9a7492eef6fef1f33961e3695e4579b9872a6f7c83aee556666d4fe" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -1813,9 +1758,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.99" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30d7a95b763d3c45903ed6c81f156801839e5ee968bb07e534c44df0fcd330c2" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" dependencies = [ "proc-macro2", "quote", @@ -1826,9 +1771,12 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.99" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "943aab3fdaaa029a6e0271b35ea10b72b943135afe9bffca82384098ad0e06a6" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] [[package]] name = "winapi-util" @@ -1839,6 +1787,65 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "windows-core" +version = "0.61.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4763c1de310c86d75a878046489e2e5ba02c649d185f21c67d4cf8a56d098980" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" + +[[package]] +name = "windows-result" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c64fd11a4fd95df68efcfee5f44a294fe71b8bc6a91993e2791938abcc712252" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2ba9642430ee452d5a7aa78d72907ebe8cfda358e8cb7918a2050581322f97" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-sys" version = "0.52.0" diff --git a/backend/Cargo.toml b/Cargo.toml similarity index 61% rename from backend/Cargo.toml rename to Cargo.toml index 4796d48..974234e 100644 --- a/backend/Cargo.toml +++ b/Cargo.toml @@ -5,7 +5,7 @@ edition = "2021" [dependencies] actix-web = "4.0" -rusqlite = { version = "0.29", features = ["bundled"] } +rusqlite = { version = "0.29", features = ["bundled", "chrono"] } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" uuid = { version = "1.0", features = ["v4"] } @@ -14,5 +14,9 @@ actix-cors = "0.6" env_logger = "0.10" log = "0.4" futures = "0.3" -bcrypt = "0.13" -anyhow = "1.0" \ No newline at end of file +bcrypt = "0.13" +anyhow = "1.0" +dotenv = "0.15.0" +chrono = { version = "0.4", features = ["serde"] } +regex = "1" +url = "2" \ No newline at end of file diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index f008b07..0000000 --- a/Dockerfile +++ /dev/null @@ -1,20 +0,0 @@ -# Stage 1: Build the Svelte frontend -FROM node:18 as frontend-builder -WORKDIR /app/frontend -COPY frontend/ . -RUN npm install -RUN npm run build - -# Stage 2: Build the Rust backend -FROM rust:1.83 as backend-builder -WORKDIR /app/backend -COPY backend/ . -RUN cargo build --release - -# Final Stage: Combine frontend and backend -FROM debian:bullseye-slim -WORKDIR /app -COPY --from=frontend-builder /app/frontend/build ./frontend/dist -COPY --from=backend-builder /app/backend/target/release/formies_be ./formies_be -EXPOSE 8080 -CMD ["./backend"] diff --git a/README.md b/README.md deleted file mode 100644 index c82b36a..0000000 --- a/README.md +++ /dev/null @@ -1,221 +0,0 @@ -# Formies - -Formies is a form management tool that allows you to create customizable forms, collect submissions, and view collected data. This project combines a Rust backend and a Svelte frontend, packaged as a single Docker container for easy deployment. - -## Features - -### 📝 Form Management - -- Create forms with customizable fields (text, number, date, etc.). -- View all created forms in a centralized dashboard. - -### 🗂️ Submissions - -- Submit responses to forms via a user-friendly interface. -- View and manage all form submissions. - -### ⚙️ Backend - -- Built with Rust using Actix-Web for high performance and scalability. -- Uses SQLite for local data storage with easy migration to PostgreSQL if needed. - -### 🎨 Frontend - -- Built with SvelteKit for a modern and lightweight user experience. -- Responsive design for use across devices. - -### 🚀 Deployment - -- Packaged as a single Docker image for seamless deployment. -- Supports CI/CD workflows with Gitea Actions, Drone CI, or GitHub Actions. - -## Folder Structure - -``` -project-root/ -├── backend/ # Backend codebase -│ ├── src/ -│ │ ├── handlers.rs # Route handlers for Actix-Web -│ │ ├── models.rs # Data models (Form, Submission) -│ │ ├── db.rs # Database initialization -│ │ ├── main.rs # Main entry point for the backend -│ │ └── ... # Additional modules -│ ├── Cargo.toml # Backend dependencies -│ └── Cargo.lock # Locked dependencies -│ -├── frontend/ # Frontend codebase -│ ├── public/ # Built static files (after `npm run build`) -│ ├── src/ -│ │ ├── lib/ # Shared utilities (e.g., API integration) -│ │ ├── routes/ # Svelte pages -│ │ │ ├── +page.svelte # Dashboard -│ │ │ └── form/ # Form-related pages -│ │ └── main.ts # Frontend entry point -│ ├── package.json # Frontend dependencies -│ ├── svelte.config.js # Svelte configuration -│ └── ... # Additional files -│ -├── Dockerfile # Combined Dockerfile for both frontend and backend -├── docker-compose.yml # Docker Compose file for deployment -├── .gitea/ # Gitea Actions workflows -│ └── workflows/ -│ └── build_and_deploy.yml -├── .drone.yml # Drone CI configuration -├── README.md # Documentation (this file) -└── ... # Other configuration files -``` - -## Prerequisites - -### Docker - -- Install Docker: [Docker Documentation](https://docs.docker.com/) - -### Rust (for development) - -- Install Rust: [Rustup Installation](https://rustup.rs/) - -### Node.js (for frontend development) - -- Install Node.js: [Node.js Downloads](https://nodejs.org/) - -## Development - -### Backend - -1. Navigate to the backend/ directory: - - ```sh - cd backend - ``` - -2. Run the backend server: - - ```sh - cargo run - ``` - - The backend will be available at [http://localhost:8080](http://localhost:8080). - -### Frontend - -1. Navigate to the frontend/ directory: - - ```sh - cd frontend - ``` - -2. Install dependencies: - - ```sh - npm install - ``` - -3. Start the development server: - - ```sh - npm run dev - ``` - - The frontend will be available at [http://localhost:5173](http://localhost:5173). - -## Deployment - -### Build the Docker Image - -1. Build the combined Docker image: - - ```sh - docker build -t your-dockerhub-username/formies-combined . - ``` - -2. Run the Docker container: - - ```sh - docker run -p 8080:8080 your-dockerhub-username/formies-combined - ``` - - Access the application at [http://localhost:8080](http://localhost:8080). - -### Using Docker Compose - -1. Deploy using `docker-compose.yml`: - - ```sh - docker-compose up -d - ``` - -2. Stop the containers: - ```sh - docker-compose down - ``` - -## CI/CD Workflow - -### Gitea Actions - -1. Create a file at `.gitea/workflows/build_and_deploy.yml`: - - ```yaml - name: Build and Deploy - - on: - push: - branches: - - main - - jobs: - build: - runs-on: ubuntu-latest - steps: - - name: Clone the repository - uses: actions/checkout@v3 - - - name: Set up Docker - uses: docker/setup-buildx-action@v2 - - - name: Log in to Docker Hub - run: echo "${{ secrets.DOCKER_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_USERNAME }}" --password-stdin - - - name: Build and Push Docker Image - run: | - docker build -t your-dockerhub-username/formies-combined . - docker push your-dockerhub-username/formies-combined:latest - - - name: Deploy to Server (optional) - run: | - ssh -o StrictHostKeyChecking=no ${{ secrets.SERVER_USER }}@${{ secrets.SERVER_IP }} << 'EOF' - docker pull your-dockerhub-username/formies-combined:latest - docker stop formies || true - docker rm formies || true - docker run -d --name formies -p 8080:8080 your-dockerhub-username/formies-combined:latest - EOF - ``` - -2. Add secrets in Gitea: - - `DOCKER_USERNAME`: Your Docker Hub username. - - `DOCKER_PASSWORD`: Your Docker Hub password. - - `SERVER_USER`: SSH username for deployment. - - `SERVER_IP`: IP address of the server. - -## API Endpoints - -**Base URL:** `http://localhost:8080` - -| Method | Endpoint | Description | -| ------ | ----------------------------- | ----------------------------------- | -| POST | `/api/forms` | Create a new form | -| GET | `/api/forms` | Get all forms | -| POST | `/api/forms/{id}/submissions` | Submit data to a form | -| GET | `/api/forms/{id}/submissions` | Get submissions for a specific form | - -## Future Enhancements - -- **Authentication:** Add user-based authentication for managing forms and submissions. -- **Export:** Allow exporting submissions to CSV or Excel. -- **Scaling:** Migrate to PostgreSQL for distributed data handling. -- **Monitoring:** Integrate tools like Prometheus and Grafana for performance monitoring. - -## License - -This project is licensed under the MIT License. See the [LICENSE](./LICENSE) file for details. diff --git a/backend/src/auth.rs b/backend/src/auth.rs deleted file mode 100644 index ca2f9d7..0000000 --- a/backend/src/auth.rs +++ /dev/null @@ -1,36 +0,0 @@ -use actix_web::{dev::Payload, http::header::AUTHORIZATION, web, Error, FromRequest, HttpRequest}; -use futures::future::{ready, Ready}; -use rusqlite::Connection; -use std::sync::{Arc, Mutex}; - -pub struct Auth { - pub user_id: String, -} - -impl FromRequest for Auth { - type Error = Error; - type Future = Ready>; - - fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future { - let db = req - .app_data::>>>() - .expect("Database connection missing"); - - if let Some(auth_header) = req.headers().get(AUTHORIZATION) { - if let Ok(auth_str) = auth_header.to_str() { - if auth_str.starts_with("Bearer ") { - let token = &auth_str[7..]; - let conn = db.lock().unwrap(); - - match super::db::validate_token(&conn, token) { - Ok(Some(user_id)) => return ready(Ok(Auth { user_id })), - Ok(None) | Err(_) => { - return ready(Err(actix_web::error::ErrorUnauthorized("Invalid token"))) - } - } - } - } - } - ready(Err(actix_web::error::ErrorUnauthorized("Missing token"))) - } -} diff --git a/backend/src/db.rs b/backend/src/db.rs deleted file mode 100644 index 89080b4..0000000 --- a/backend/src/db.rs +++ /dev/null @@ -1,123 +0,0 @@ -use anyhow::{Context, Result as AnyhowResult}; -use bcrypt::{hash, verify, DEFAULT_COST}; // Add bcrypt dependency for password hashing -use rusqlite::{params, Connection, OptionalExtension}; -use uuid::Uuid; // UUID for generating unique IDs // Import anyhow - -pub fn init_db() -> AnyhowResult { - let conn = Connection::open("form_data.db").context("Failed to open the database")?; - - // Create tables - conn.execute( - "CREATE TABLE IF NOT EXISTS forms ( - id TEXT PRIMARY KEY, - name TEXT NOT NULL, - fields TEXT NOT NULL, - created_at DATETIME DEFAULT CURRENT_TIMESTAMP - )", - [], - )?; - - conn.execute( - "CREATE TABLE IF NOT EXISTS submissions ( - id TEXT PRIMARY KEY, - form_id TEXT NOT NULL, - data TEXT NOT NULL, - created_at DATETIME DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (form_id) REFERENCES forms (id) ON DELETE CASCADE - )", - [], - )?; - - // Add a table for users - conn.execute( - "CREATE TABLE IF NOT EXISTS users ( - id TEXT PRIMARY KEY, - username TEXT NOT NULL UNIQUE, - password TEXT NOT NULL, -- Store a hashed password - token TEXT UNIQUE -- Optional: For token-based auth - )", - [], - )?; - - // Setup initial admin after creating the tables - setup_initial_admin(&conn)?; - - Ok(conn) -} - -pub fn setup_initial_admin(conn: &Connection) -> AnyhowResult<()> { - add_admin_user(conn)?; - Ok(()) -} - -pub fn add_admin_user(conn: &Connection) -> AnyhowResult<()> { - // Check if admin user already exists - let mut stmt = conn - .prepare("SELECT id FROM users WHERE username = ?1") - .context("Failed to prepare query for checking admin user")?; - if stmt.exists(params!["admin"])? { - return Ok(()); - } - - // Generate a UUID for the admin user - let admin_id = Uuid::new_v4().to_string(); - - // Hash the password before storing it - let hashed_password = hash("admin", DEFAULT_COST).context("Failed to hash password")?; - - // Add admin user with hashed password - conn.execute( - "INSERT INTO users (id, username, password) VALUES (?1, ?2, ?3)", - params![admin_id, "admin", hashed_password], - ) - .context("Failed to insert admin user into the database")?; - - Ok(()) -} - -// Add a function to validate a token -pub fn validate_token(conn: &Connection, token: &str) -> AnyhowResult> { - let mut stmt = conn - .prepare("SELECT id FROM users WHERE token = ?1") - .context("Failed to prepare query for validating token")?; - let user_id: Option = stmt - .query_row(params![token], |row| row.get(0)) - .optional() - .context("Failed to retrieve user ID for the given token")?; - Ok(user_id) -} - -// Add a function to authenticate users (for login) -pub fn authenticate_user( - conn: &Connection, - username: &str, - password: &str, -) -> AnyhowResult> { - let mut stmt = conn - .prepare("SELECT id, password FROM users WHERE username = ?1") - .context("Failed to prepare query for authenticating user")?; - let mut rows = stmt - .query(params![username]) - .context("Failed to execute query for authenticating user")?; - - if let Some(row) = rows.next()? { - let user_id: String = row.get(0)?; - let stored_password: String = row.get(1)?; - - // Use bcrypt to verify the hashed password - if verify(password, &stored_password).context("Failed to verify password")? { - return Ok(Some(user_id)); - } - } - Ok(None) -} - -// Add a function to generate and save a token for a user -pub fn generate_token_for_user(conn: &Connection, user_id: &str, token: &str) -> AnyhowResult<()> { - conn.execute( - "UPDATE users SET token = ?1 WHERE id = ?2", - params![token, user_id], - ) - .context("Failed to update token for user")?; - Ok(()) -} diff --git a/backend/src/handlers.rs b/backend/src/handlers.rs deleted file mode 100644 index 3c9bd3e..0000000 --- a/backend/src/handlers.rs +++ /dev/null @@ -1,240 +0,0 @@ -use crate::models::{AdminUser, Claims, Form, LoginCredentials, Submission}; -use actix_web::{web, HttpResponse, Responder}; -use argon2::{ - password_hash::{rand_core::OsRng, PasswordHash, PasswordHasher, PasswordVerifier, SaltString}, - Argon2, -}; -use jsonwebtoken::{encode, EncodingKey, Header}; -use rusqlite::{params, Connection}; -use serde::{Deserialize, Serialize}; -use std::sync::{Arc, Mutex}; -use uuid::Uuid; - -use crate::auth::Auth; - -// Structs for requests and responses -#[derive(Deserialize)] -pub struct LoginRequest { - pub username: String, - pub password: String, -} - -#[derive(Serialize)] -pub struct LoginResponse { - pub token: String, -} - -// Public: Login handler -pub async fn login( - db: web::Data>>, - login_request: web::Json, -) -> impl Responder { - let conn = db.lock().unwrap(); - let user_id = - match crate::db::authenticate_user(&conn, &login_request.username, &login_request.password) - { - Ok(Some(user_id)) => user_id, - Ok(None) => return HttpResponse::Unauthorized().body("Invalid username or password"), - Err(_) => return HttpResponse::InternalServerError().body("Database error"), - }; - - let token = Uuid::new_v4().to_string(); - if let Err(_) = crate::db::generate_token_for_user(&conn, &user_id, &token) { - return HttpResponse::InternalServerError().body("Failed to generate token"); - } - - HttpResponse::Ok().json(LoginResponse { token }) -} - -// Public: Submit a form -pub async fn submit_form( - db: web::Data>>, - path: web::Path, - submission: web::Form, -) -> impl Responder { - let conn = db.lock().unwrap(); - let submission_id = Uuid::new_v4().to_string(); - let form_id = path.into_inner(); - let submission_json = serde_json::to_string(&submission.into_inner()).unwrap(); - - match conn.execute( - "INSERT INTO submissions (id, form_id, data) VALUES (?1, ?2, ?3)", - params![submission_id, form_id, submission_json], - ) { - Ok(_) => HttpResponse::Ok().json(submission_id), - Err(e) => HttpResponse::InternalServerError().body(format!("Error: {}", e)), - } -} - -// Protected: Create a new form -pub async fn create_form( - db: web::Data>>, - auth: Auth, - form: web::Json, -) -> impl Responder { - println!("Authenticated user: {}", auth.user_id); - let conn = db.lock().unwrap(); - let form_id = Uuid::new_v4().to_string(); - let form_json = serde_json::to_string(&form.fields).unwrap(); - - match conn.execute( - "INSERT INTO forms (id, name, fields) VALUES (?1, ?2, ?3)", - params![form_id, form.name, form_json], - ) { - Ok(_) => HttpResponse::Ok().json(form_id), - Err(e) => HttpResponse::InternalServerError().body(format!("Error: {}", e)), - } -} - -// Protected: Get all forms -pub async fn get_forms(db: web::Data>>, auth: Auth) -> impl Responder { - println!("Authenticated user: {}", auth.user_id); - let conn = db.lock().unwrap(); - - let mut stmt = match conn.prepare("SELECT id, name, fields FROM forms") { - Ok(stmt) => stmt, - Err(e) => return HttpResponse::InternalServerError().body(format!("Error: {}", e)), - }; - - let forms_iter = stmt - .query_map([], |row| { - let id: Option = row.get(0)?; - let name: String = row.get(1)?; - let fields: String = row.get(2)?; - let fields = serde_json::from_str(&fields).unwrap(); - Ok(crate::models::Form { id, name, fields }) - }) - .unwrap(); - - let forms: Vec = forms_iter.filter_map(|f| f.ok()).collect(); - HttpResponse::Ok().json(forms) -} - -// Protected: Get submissions for a form -pub async fn get_submissions( - db: web::Data>>, - auth: Auth, - path: web::Path, -) -> impl Responder { - println!("Authenticated user: {}", auth.user_id); - let conn = db.lock().unwrap(); - let form_id = path.into_inner(); - - let mut stmt = - match conn.prepare("SELECT id, form_id, data FROM submissions WHERE form_id = ?1") { - Ok(stmt) => stmt, - Err(e) => return HttpResponse::InternalServerError().body(format!("Error: {}", e)), - }; - - let submissions_iter = stmt - .query_map([form_id], |row| { - let id: String = row.get(0)?; - let form_id: String = row.get(1)?; - let data: String = row.get(2)?; - let data = serde_json::from_str(&data).unwrap(); - Ok(crate::models::Submission { id, form_id, data }) - }) - .unwrap(); - - let submissions: Vec = - submissions_iter.filter_map(|s| s.ok()).collect(); - HttpResponse::Ok().json(submissions) -} - -pub async fn admin_login( - db: web::Data>>, - credentials: web::Json, -) -> impl Responder { - let conn = match db.lock() { - Ok(conn) => conn, - Err(_) => return HttpResponse::InternalServerError().body("Database lock error"), - }; - - let mut stmt = - match conn.prepare("SELECT username, password_hash FROM admin_users WHERE username = ?1") { - Ok(stmt) => stmt, - Err(e) => { - return HttpResponse::InternalServerError().body(format!("Database error: {}", e)) - } - }; - - let admin: Option = match stmt.query_row([&credentials.username], |row| { - Ok(AdminUser { - username: row.get(0)?, - password_hash: row.get(1)?, - }) - }) { - Ok(admin) => Some(admin), - Err(rusqlite::Error::QueryReturnedNoRows) => None, // No user found - Err(e) => return HttpResponse::InternalServerError().body(format!("Query error: {}", e)), - }; - - match admin { - Some(user) => { - let parsed_hash = match PasswordHash::new(&user.password_hash) { - Ok(hash) => hash, - Err(_) => { - return HttpResponse::InternalServerError() - .body("Invalid password hash format in database") - } - }; - - let argon2 = Argon2::default(); - let is_valid = argon2 - .verify_password(credentials.password.as_bytes(), &parsed_hash) - .is_ok(); - - if is_valid { - let expiration = match SystemTime::now().duration_since(UNIX_EPOCH) { - Ok(duration) => duration.as_secs() as usize + 24 * 3600, - Err(_) => return HttpResponse::InternalServerError().body("System time error"), - }; - - let claims = Claims { - sub: user.username, - exp: expiration, - }; - - let token = match encode( - &Header::default(), - &claims, - &EncodingKey::from_secret("your-secret-key".as_ref()), - ) { - Ok(token) => token, - Err(_) => { - return HttpResponse::InternalServerError().body("Token generation error") - } - }; - - HttpResponse::Ok().json(json!({ "token": token })) - } else { - HttpResponse::Unauthorized().json(json!({ "error": "Invalid credentials" })) - } - } - None => HttpResponse::Unauthorized().json(json!({ "error": "Invalid credentials" })), - } -} - -pub async fn create_admin( - db: web::Data>>, - user: web::Json, -) -> impl Responder { - let conn = db.lock().unwrap(); - - let salt = SaltString::generate(&mut OsRng); - let argon2 = Argon2::default(); - let password_hash = argon2 - .hash_password(user.password.as_bytes(), &salt) - .unwrap() - .to_string(); - - match conn.execute( - "INSERT INTO admin_users (username, password_hash) VALUES (?1, ?2)", - params![user.username, password_hash], - ) { - Ok(_) => HttpResponse::Ok().json(json!({ - "message": "Admin user created successfully" - })), - Err(e) => HttpResponse::InternalServerError().body(format!("Error: {}", e)), - } -} diff --git a/backend/src/main.rs b/backend/src/main.rs deleted file mode 100644 index d2173bd..0000000 --- a/backend/src/main.rs +++ /dev/null @@ -1,64 +0,0 @@ -use actix_cors::Cors; -use actix_files as fs; -use actix_web::{web, App, HttpServer}; -use std::sync::{Arc, Mutex}; - -mod auth; -mod db; -mod handlers; // Ensure handlers.rs exists -mod middleware; // Ensure middleware.rs exists // Ensure db.rs exists -mod models; - -use crate::middleware::AuthMiddleware; -use handlers::{admin_login, create_admin, create_form, get_forms, get_submissions, submit_form}; - -pub fn configure_routes(cfg: &mut web::ServiceConfig) { - cfg.service( - web::scope("") - .route("/forms/{id}", web::post().to(submit_form)) - .route("/admin/login", web::post().to(admin_login)) - .route("/admin/create", web::post().to(create_admin)), - ); - - cfg.service( - web::scope("") - .wrap(AuthMiddleware) - .route("/forms", web::get().to(get_forms)) - .route("/forms", web::post().to(create_form)) - .route("/forms/{id}/submissions", web::get().to(get_submissions)), - ); -} - -#[actix_web::main] -async fn main() -> std::io::Result<()> { - env_logger::init(); - let db = Arc::new(Mutex::new( - db::init_db().expect("Failed to initialize the database"), - )); - - HttpServer::new(move || { - App::new() - .wrap( - Cors::default() - .allow_any_origin() - .allow_any_header() - .allow_any_method(), - ) - .app_data(web::Data::new(db.clone())) - .service(fs::Files::new("/", "./frontend/dist").index_file("index.html")) - .route("/login", web::post().to(handlers::login)) // Public: Login - .route( - "/forms/{id}/submissions", - web::post().to(handlers::submit_form), // Public: Submit form - ) - .route("/forms", web::post().to(handlers::create_form)) // Protected - .route("/forms", web::get().to(handlers::get_forms)) // Protected - .route( - "/forms/{id}/submissions", - web::get().to(handlers::get_submissions), // Protected - ) - }) - .bind("127.0.0.1:8080")? - .run() - .await -} diff --git a/backend/src/middleware.rs b/backend/src/middleware.rs deleted file mode 100644 index fbdb7b0..0000000 --- a/backend/src/middleware.rs +++ /dev/null @@ -1,87 +0,0 @@ -use crate::models::Claims; -use actix_web::body::{BoxBody, MessageBody}; -use actix_web::dev::{forward_ready, Service, ServiceRequest, ServiceResponse, Transform}; -use actix_web::{Error, HttpResponse}; -use futures::future::{ok, Ready}; -use serde_json::json; -use std::future::Future; -use std::pin::Pin; - -pub struct AuthMiddleware; - -impl Transform for AuthMiddleware -where - S: Service, Error = Error>, - S::Future: 'static, - B: MessageBody + 'static, -{ - type Response = ServiceResponse; // Changed to BoxBody - type Error = Error; - type Transform = AuthMiddlewareService; - type InitError = (); - type Future = Ready>; - - fn new_transform(&self, service: S) -> Self::Future { - ok(AuthMiddlewareService { service }) - } -} - -pub struct AuthMiddlewareService { - service: S, -} - -impl Service for AuthMiddlewareService -where - S: Service, Error = Error>, - S::Future: 'static, - B: MessageBody + 'static, -{ - type Response = ServiceResponse; // Changed to BoxBody - type Error = Error; - type Future = Pin>>>; - - forward_ready!(service); - - fn call(&self, req: ServiceRequest) -> Self::Future { - if req.path() == "/admin/login" || req.path() == "/admin/create" { - let fut = self.service.call(req); - return Box::pin(async move { - let res = fut.await?; - Ok(res.map_into_boxed_body()) // Convert the response body to BoxBody - }); - } - - let auth_header = req.headers().get("Authorization"); - match auth_header { - Some(header) => { - let token = header.to_str().unwrap_or("").replace("Bearer ", ""); - if verify_token(&token) { - let fut = self.service.call(req); - Box::pin(async move { - let res = fut.await?; - Ok(res.map_into_boxed_body()) // Convert the response body to BoxBody - }) - } else { - let (request, _) = req.into_parts(); - let response = HttpResponse::Unauthorized() - .json(json!({"error": "Invalid token"})) - .map_into_boxed_body(); - Box::pin(async move { Ok(ServiceResponse::new(request, response)) }) - } - } - None => { - let (request, _) = req.into_parts(); - let response = HttpResponse::Unauthorized() - .json(json!({"error": "No authorization token"})) - .map_into_boxed_body(); - Box::pin(async move { Ok(ServiceResponse::new(request, response)) }) - } - } - } -} - -pub fn verify_token(token: &str) -> bool { - let validation = jsonwebtoken::Validation::default(); - let key = jsonwebtoken::DecodingKey::from_secret("your-secret-key".as_ref()); - jsonwebtoken::decode::(token, &key, &validation).is_ok() -} diff --git a/backend/src/models.rs b/backend/src/models.rs deleted file mode 100644 index b837049..0000000 --- a/backend/src/models.rs +++ /dev/null @@ -1,33 +0,0 @@ -use serde::{Deserialize, Serialize}; - -#[derive(Serialize, Deserialize, Debug)] -pub struct Form { - pub id: Option, - pub name: String, - pub fields: serde_json::Value, // JSON array of form fields -} - -#[derive(Serialize, Deserialize)] -pub struct Submission { - pub id: String, - pub form_id: String, - pub data: serde_json::Value, // JSON of submission data -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct AdminUser { - pub username: String, - pub password_hash: String, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct LoginCredentials { - pub username: String, - pub password: String, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct Claims { - pub sub: String, - pub(crate) exp: usize, -} diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index 8e90eef..0000000 --- a/docker-compose.yml +++ /dev/null @@ -1,9 +0,0 @@ -version: "3.8" - -services: - formies: - image: your-dockerhub-username/formies-combined:latest - container_name: formies-app - ports: - - "8080:8080" # Expose the application on port 8080 - restart: always diff --git a/backend/form_data.db b/form_data.db similarity index 100% rename from backend/form_data.db rename to form_data.db diff --git a/frontend/.gitignore b/frontend/.gitignore deleted file mode 100644 index 3b462cb..0000000 --- a/frontend/.gitignore +++ /dev/null @@ -1,23 +0,0 @@ -node_modules - -# Output -.output -.vercel -.netlify -.wrangler -/.svelte-kit -/build - -# OS -.DS_Store -Thumbs.db - -# Env -.env -.env.* -!.env.example -!.env.test - -# Vite -vite.config.js.timestamp-* -vite.config.ts.timestamp-* diff --git a/frontend/.npmrc b/frontend/.npmrc deleted file mode 100644 index b6f27f1..0000000 --- a/frontend/.npmrc +++ /dev/null @@ -1 +0,0 @@ -engine-strict=true diff --git a/frontend/.prettierignore b/frontend/.prettierignore deleted file mode 100644 index ab78a95..0000000 --- a/frontend/.prettierignore +++ /dev/null @@ -1,4 +0,0 @@ -# Package Managers -package-lock.json -pnpm-lock.yaml -yarn.lock diff --git a/frontend/.prettierrc b/frontend/.prettierrc deleted file mode 100644 index 3f7802c..0000000 --- a/frontend/.prettierrc +++ /dev/null @@ -1,15 +0,0 @@ -{ - "useTabs": true, - "singleQuote": true, - "trailingComma": "none", - "printWidth": 100, - "plugins": ["prettier-plugin-svelte"], - "overrides": [ - { - "files": "*.svelte", - "options": { - "parser": "svelte" - } - } - ] -} diff --git a/frontend/README.md b/frontend/README.md deleted file mode 100644 index b5b2950..0000000 --- a/frontend/README.md +++ /dev/null @@ -1,38 +0,0 @@ -# sv - -Everything you need to build a Svelte project, powered by [`sv`](https://github.com/sveltejs/cli). - -## Creating a project - -If you're seeing this, you've probably already done this step. Congrats! - -```bash -# create a new project in the current directory -npx sv create - -# create a new project in my-app -npx sv create my-app -``` - -## Developing - -Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server: - -```bash -npm run dev - -# or start the server and open the app in a new browser tab -npm run dev -- --open -``` - -## Building - -To create a production version of your app: - -```bash -npm run build -``` - -You can preview the production build with `npm run preview`. - -> To deploy your app, you may need to install an [adapter](https://svelte.dev/docs/kit/adapters) for your target environment. diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js deleted file mode 100644 index aa5987f..0000000 --- a/frontend/eslint.config.js +++ /dev/null @@ -1,34 +0,0 @@ -import prettier from 'eslint-config-prettier'; -import js from '@eslint/js'; -import { includeIgnoreFile } from '@eslint/compat'; -import svelte from 'eslint-plugin-svelte'; -import globals from 'globals'; -import { fileURLToPath } from 'node:url'; -import ts from 'typescript-eslint'; -const gitignorePath = fileURLToPath(new URL('./.gitignore', import.meta.url)); - -export default ts.config( - includeIgnoreFile(gitignorePath), - js.configs.recommended, - ...ts.configs.recommended, - ...svelte.configs['flat/recommended'], - prettier, - ...svelte.configs['flat/prettier'], - { - languageOptions: { - globals: { - ...globals.browser, - ...globals.node - } - } - }, - { - files: ['**/*.svelte'], - - languageOptions: { - parserOptions: { - parser: ts.parser - } - } - } -); diff --git a/frontend/package-lock.json b/frontend/package-lock.json deleted file mode 100644 index fed880a..0000000 --- a/frontend/package-lock.json +++ /dev/null @@ -1,3652 +0,0 @@ -{ - "name": "formies-fe", - "version": "0.0.1", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "formies-fe", - "version": "0.0.1", - "dependencies": { - "@types/uuid": "^10.0.0", - "axios": "^1.7.9" - }, - "devDependencies": { - "@eslint/compat": "^1.2.3", - "@sveltejs/adapter-auto": "^3.0.0", - "@sveltejs/adapter-node": "^5.2.11", - "@sveltejs/kit": "^2.0.0", - "@sveltejs/vite-plugin-svelte": "^4.0.0", - "eslint": "^9.7.0", - "eslint-config-prettier": "^9.1.0", - "eslint-plugin-svelte": "^2.36.0", - "globals": "^15.0.0", - "prettier": "^3.3.2", - "prettier-plugin-svelte": "^3.2.6", - "svelte": "^5.0.0", - "svelte-check": "^4.0.0", - "typescript": "^5.0.0", - "typescript-eslint": "^8.0.0", - "vite": "^5.4.11" - } - }, - "node_modules/@ampproject/remapping": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", - "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", - "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", - "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", - "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", - "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", - "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", - "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", - "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", - "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", - "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", - "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", - "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", - "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", - "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", - "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", - "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", - "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", - "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", - "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", - "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", - "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", - "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", - "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", - "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.1.tgz", - "integrity": "sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA==", - "dev": true, - "license": "MIT", - "dependencies": { - "eslint-visitor-keys": "^3.4.3" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" - } - }, - "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint-community/regexpp": { - "version": "4.12.1", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", - "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" - } - }, - "node_modules/@eslint/compat": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@eslint/compat/-/compat-1.2.4.tgz", - "integrity": "sha512-S8ZdQj/N69YAtuqFt7653jwcvuUj131+6qGLUyDqfDg1OIoBQ66OCuXC473YQfO2AaxITTutiRQiDwoo7ZLYyg==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "peerDependencies": { - "eslint": "^9.10.0" - }, - "peerDependenciesMeta": { - "eslint": { - "optional": true - } - } - }, - "node_modules/@eslint/config-array": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.19.1.tgz", - "integrity": "sha512-fo6Mtm5mWyKjA/Chy1BYTdn5mGJoDNjC7C64ug20ADsRDGrA85bN3uK3MaKbeRkRuuIEAR5N33Jr1pbm411/PA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/object-schema": "^2.1.5", - "debug": "^4.3.1", - "minimatch": "^3.1.2" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/core": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.9.1.tgz", - "integrity": "sha512-GuUdqkyyzQI5RMIWkHhvTWLCyLo1jNK3vzkSyaExH5kHPDHcuL2VOpHjmMY+y3+NC69qAKToBqldTBgYeLSr9Q==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@types/json-schema": "^7.0.15" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/eslintrc": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.2.0.tgz", - "integrity": "sha512-grOjVNN8P3hjJn/eIETF1wwd12DdnwFDoyceUJLYYdkpbwq3nLi+4fqrTAONx7XDALqlL220wC/RHSC/QTI/0w==", - "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^10.0.1", - "globals": "^14.0.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint/eslintrc/node_modules/globals": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", - "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@eslint/js": { - "version": "9.17.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.17.0.tgz", - "integrity": "sha512-Sxc4hqcs1kTu0iID3kcZDW3JHq2a77HO9P8CP6YEA/FpH3Ll8UXE2r/86Rz9YJLKme39S9vU5OWNjC6Xl0Cr3w==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/object-schema": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.5.tgz", - "integrity": "sha512-o0bhxnL89h5Bae5T318nFoFzGy+YE5i/gGkoPAgkmTVdRKTiv3p8JHevPiPaMwoloKfEiiaHlawCqaZMqRm+XQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/plugin-kit": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.4.tgz", - "integrity": "sha512-zSkKow6H5Kdm0ZUQUB2kV5JIXqoG0+uH5YADhaEHswm664N9Db8dXSi0nMJpacpMf+MyyglF1vnZohpEg5yUtg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "levn": "^0.4.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@humanfs/core": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", - "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/@humanfs/node": { - "version": "0.16.6", - "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", - "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@humanfs/core": "^0.19.1", - "@humanwhocodes/retry": "^0.3.0" - }, - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", - "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=12.22" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@humanwhocodes/retry": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.1.tgz", - "integrity": "sha512-c7hNEllBlenFTHBky65mhq8WD2kbN9Q6gk0bTk8lSBvc554jpXSkST1iePudpt7+A/AQvuHs9EMqjHDXMY1lrA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.8", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", - "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/set-array": "^1.2.1", - "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/set-array": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", - "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", - "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.25", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", - "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@nodelib/fs.scandir": { - "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", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.stat": { - "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" - } - }, - "node_modules/@nodelib/fs.walk": { - "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", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@polka/url": { - "version": "1.0.0-next.28", - "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.28.tgz", - "integrity": "sha512-8LduaNlMZGwdZ6qWrKlfa+2M4gahzFkprZiAt2TF8uS0qQgBizKXpXURqvTJ4WtmupWxaLqjRb2UCTe72mu+Aw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@rollup/plugin-commonjs": { - "version": "28.0.2", - "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-28.0.2.tgz", - "integrity": "sha512-BEFI2EDqzl+vA1rl97IDRZ61AIwGH093d9nz8+dThxJNH8oSoB7MjWvPCX3dkaK1/RCJ/1v/R1XB15FuSs0fQw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@rollup/pluginutils": "^5.0.1", - "commondir": "^1.0.1", - "estree-walker": "^2.0.2", - "fdir": "^6.2.0", - "is-reference": "1.2.1", - "magic-string": "^0.30.3", - "picomatch": "^4.0.2" - }, - "engines": { - "node": ">=16.0.0 || 14 >= 14.17" - }, - "peerDependencies": { - "rollup": "^2.68.0||^3.0.0||^4.0.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } - } - }, - "node_modules/@rollup/plugin-commonjs/node_modules/is-reference": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz", - "integrity": "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "*" - } - }, - "node_modules/@rollup/plugin-json": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/@rollup/plugin-json/-/plugin-json-6.1.0.tgz", - "integrity": "sha512-EGI2te5ENk1coGeADSIwZ7G2Q8CJS2sF120T7jLw4xFw9n7wIOXHo+kIYRAoVpJAN+kmqZSoO3Fp4JtoNF4ReA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@rollup/pluginutils": "^5.1.0" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } - } - }, - "node_modules/@rollup/plugin-node-resolve": { - "version": "16.0.0", - "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-16.0.0.tgz", - "integrity": "sha512-0FPvAeVUT/zdWoO0jnb/V5BlBsUSNfkIOtFHzMO4H9MOklrmQFY6FduVHKucNb/aTFxvnGhj4MNj/T1oNdDfNg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@rollup/pluginutils": "^5.0.1", - "@types/resolve": "1.20.2", - "deepmerge": "^4.2.2", - "is-module": "^1.0.0", - "resolve": "^1.22.1" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "rollup": "^2.78.0||^3.0.0||^4.0.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } - } - }, - "node_modules/@rollup/pluginutils": { - "version": "5.1.4", - "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.4.tgz", - "integrity": "sha512-USm05zrsFxYLPdWWq+K3STlWiT/3ELn3RcV5hJMghpeAIhxfsUIg6mt12CBJBInWMV4VneoV7SfGv8xIwo2qNQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0", - "estree-walker": "^2.0.2", - "picomatch": "^4.0.2" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } - } - }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.29.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.29.1.tgz", - "integrity": "sha512-ssKhA8RNltTZLpG6/QNkCSge+7mBQGUqJRisZ2MDQcEGaK93QESEgWK2iOpIDZ7k9zPVkG5AS3ksvD5ZWxmItw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.29.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.29.1.tgz", - "integrity": "sha512-CaRfrV0cd+NIIcVVN/jx+hVLN+VRqnuzLRmfmlzpOzB87ajixsN/+9L5xNmkaUUvEbI5BmIKS+XTwXsHEb65Ew==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.29.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.29.1.tgz", - "integrity": "sha512-2ORr7T31Y0Mnk6qNuwtyNmy14MunTAMx06VAPI6/Ju52W10zk1i7i5U3vlDRWjhOI5quBcrvhkCHyF76bI7kEw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.29.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.29.1.tgz", - "integrity": "sha512-j/Ej1oanzPjmN0tirRd5K2/nncAhS9W6ICzgxV+9Y5ZsP0hiGhHJXZ2JQ53iSSjj8m6cRY6oB1GMzNn2EUt6Ng==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.29.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.29.1.tgz", - "integrity": "sha512-91C//G6Dm/cv724tpt7nTyP+JdN12iqeXGFM1SqnljCmi5yTXriH7B1r8AD9dAZByHpKAumqP1Qy2vVNIdLZqw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.29.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.29.1.tgz", - "integrity": "sha512-hEioiEQ9Dec2nIRoeHUP6hr1PSkXzQaCUyqBDQ9I9ik4gCXQZjJMIVzoNLBRGet+hIUb3CISMh9KXuCcWVW/8w==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.29.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.29.1.tgz", - "integrity": "sha512-Py5vFd5HWYN9zxBv3WMrLAXY3yYJ6Q/aVERoeUFwiDGiMOWsMs7FokXihSOaT/PMWUty/Pj60XDQndK3eAfE6A==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.29.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.29.1.tgz", - "integrity": "sha512-RiWpGgbayf7LUcuSNIbahr0ys2YnEERD4gYdISA06wa0i8RALrnzflh9Wxii7zQJEB2/Eh74dX4y/sHKLWp5uQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.29.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.29.1.tgz", - "integrity": "sha512-Z80O+taYxTQITWMjm/YqNoe9d10OX6kDh8X5/rFCMuPqsKsSyDilvfg+vd3iXIqtfmp+cnfL1UrYirkaF8SBZA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.29.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.29.1.tgz", - "integrity": "sha512-fOHRtF9gahwJk3QVp01a/GqS4hBEZCV1oKglVVq13kcK3NeVlS4BwIFzOHDbmKzt3i0OuHG4zfRP0YoG5OF/rA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loongarch64-gnu": { - "version": "4.29.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.29.1.tgz", - "integrity": "sha512-5a7q3tnlbcg0OodyxcAdrrCxFi0DgXJSoOuidFUzHZ2GixZXQs6Tc3CHmlvqKAmOs5eRde+JJxeIf9DonkmYkw==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.29.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.29.1.tgz", - "integrity": "sha512-9b4Mg5Yfz6mRnlSPIdROcfw1BU22FQxmfjlp/CShWwO3LilKQuMISMTtAu/bxmmrE6A902W2cZJuzx8+gJ8e9w==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.29.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.29.1.tgz", - "integrity": "sha512-G5pn0NChlbRM8OJWpJFMX4/i8OEU538uiSv0P6roZcbpe/WfhEO+AT8SHVKfp8qhDQzaz7Q+1/ixMy7hBRidnQ==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.29.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.29.1.tgz", - "integrity": "sha512-WM9lIkNdkhVwiArmLxFXpWndFGuOka4oJOZh8EP3Vb8q5lzdSCBuhjavJsw68Q9AKDGeOOIHYzYm4ZFvmWez5g==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.29.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.29.1.tgz", - "integrity": "sha512-87xYCwb0cPGZFoGiErT1eDcssByaLX4fc0z2nRM6eMtV9njAfEE6OW3UniAoDhX4Iq5xQVpE6qO9aJbCFumKYQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.29.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.29.1.tgz", - "integrity": "sha512-xufkSNppNOdVRCEC4WKvlR1FBDyqCSCpQeMMgv9ZyXqqtKBfkw1yfGMTUTs9Qsl6WQbJnsGboWCp7pJGkeMhKA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.29.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.29.1.tgz", - "integrity": "sha512-F2OiJ42m77lSkizZQLuC+jiZ2cgueWQL5YC9tjo3AgaEw+KJmVxHGSyQfDUoYR9cci0lAywv2Clmckzulcq6ig==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.29.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.29.1.tgz", - "integrity": "sha512-rYRe5S0FcjlOBZQHgbTKNrqxCBUmgDJem/VQTCcTnA2KCabYSWQDrytOzX7avb79cAAweNmMUb/Zw18RNd4mng==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.29.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.29.1.tgz", - "integrity": "sha512-+10CMg9vt1MoHj6x1pxyjPSMjHTIlqs8/tBztXvPAx24SKs9jwVnKqHJumlH/IzhaPUaj3T6T6wfZr8okdXaIg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@sveltejs/adapter-auto": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/@sveltejs/adapter-auto/-/adapter-auto-3.3.1.tgz", - "integrity": "sha512-5Sc7WAxYdL6q9j/+D0jJKjGREGlfIevDyHSQ2eNETHcB1TKlQWHcAo8AS8H1QdjNvSXpvOwNjykDUHPEAyGgdQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "import-meta-resolve": "^4.1.0" - }, - "peerDependencies": { - "@sveltejs/kit": "^2.0.0" - } - }, - "node_modules/@sveltejs/adapter-node": { - "version": "5.2.11", - "resolved": "https://registry.npmjs.org/@sveltejs/adapter-node/-/adapter-node-5.2.11.tgz", - "integrity": "sha512-lR7/dfUaKFf3aI408KRDy/BVDYoqUws7zNOJz2Hl4JoshlTnMgdha3brXBRFXB+cWtYvJjjPhvmq3xqpbioi4w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@rollup/plugin-commonjs": "^28.0.1", - "@rollup/plugin-json": "^6.1.0", - "@rollup/plugin-node-resolve": "^16.0.0", - "rollup": "^4.9.5" - }, - "peerDependencies": { - "@sveltejs/kit": "^2.4.0" - } - }, - "node_modules/@sveltejs/kit": { - "version": "2.15.1", - "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.15.1.tgz", - "integrity": "sha512-8t7D3hQHbUDMiaQ2RVnjJJ/+Ur4Fn/tkeySJCsHtX346Q9cp3LAnav8xXdfuqYNJwpUGX0x3BqF1uvbmXQw93A==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "@types/cookie": "^0.6.0", - "cookie": "^0.6.0", - "devalue": "^5.1.0", - "esm-env": "^1.2.1", - "import-meta-resolve": "^4.1.0", - "kleur": "^4.1.5", - "magic-string": "^0.30.5", - "mrmime": "^2.0.0", - "sade": "^1.8.1", - "set-cookie-parser": "^2.6.0", - "sirv": "^3.0.0", - "tiny-glob": "^0.2.9" - }, - "bin": { - "svelte-kit": "svelte-kit.js" - }, - "engines": { - "node": ">=18.13" - }, - "peerDependencies": { - "@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0", - "svelte": "^4.0.0 || ^5.0.0-next.0", - "vite": "^5.0.3 || ^6.0.0" - } - }, - "node_modules/@sveltejs/vite-plugin-svelte": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-4.0.4.tgz", - "integrity": "sha512-0ba1RQ/PHen5FGpdSrW7Y3fAMQjrXantECALeOiOdBdzR5+5vPP6HVZRLmZaQL+W8m++o+haIAKq5qT+MiZ7VA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@sveltejs/vite-plugin-svelte-inspector": "^3.0.0-next.0||^3.0.0", - "debug": "^4.3.7", - "deepmerge": "^4.3.1", - "kleur": "^4.1.5", - "magic-string": "^0.30.12", - "vitefu": "^1.0.3" - }, - "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22" - }, - "peerDependencies": { - "svelte": "^5.0.0-next.96 || ^5.0.0", - "vite": "^5.0.0" - } - }, - "node_modules/@sveltejs/vite-plugin-svelte-inspector": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte-inspector/-/vite-plugin-svelte-inspector-3.0.1.tgz", - "integrity": "sha512-2CKypmj1sM4GE7HjllT7UKmo4Q6L5xFRd7VMGEWhYnZ+wc6AUVU01IBd7yUi6WnFndEwWoMNOd6e8UjoN0nbvQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.3.7" - }, - "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22" - }, - "peerDependencies": { - "@sveltejs/vite-plugin-svelte": "^4.0.0-next.0||^4.0.0", - "svelte": "^5.0.0-next.96 || ^5.0.0", - "vite": "^5.0.0" - } - }, - "node_modules/@types/cookie": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", - "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/estree": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", - "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/json-schema": { - "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/resolve": { - "version": "1.20.2", - "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", - "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/uuid": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz", - "integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==", - "license": "MIT" - }, - "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.18.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.18.2.tgz", - "integrity": "sha512-adig4SzPLjeQ0Tm+jvsozSGiCliI2ajeURDGHjZ2llnA+A67HihCQ+a3amtPhUakd1GlwHxSRvzOZktbEvhPPg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.18.2", - "@typescript-eslint/type-utils": "8.18.2", - "@typescript-eslint/utils": "8.18.2", - "@typescript-eslint/visitor-keys": "8.18.2", - "graphemer": "^1.4.0", - "ignore": "^5.3.1", - "natural-compare": "^1.4.0", - "ts-api-utils": "^1.3.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.8.0" - } - }, - "node_modules/@typescript-eslint/parser": { - "version": "8.18.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.18.2.tgz", - "integrity": "sha512-y7tcq4StgxQD4mDr9+Jb26dZ+HTZ/SkfqpXSiqeUXZHxOUyjWDKsmwKhJ0/tApR08DgOhrFAoAhyB80/p3ViuA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/scope-manager": "8.18.2", - "@typescript-eslint/types": "8.18.2", - "@typescript-eslint/typescript-estree": "8.18.2", - "@typescript-eslint/visitor-keys": "8.18.2", - "debug": "^4.3.4" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.8.0" - } - }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "8.18.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.18.2.tgz", - "integrity": "sha512-YJFSfbd0CJjy14r/EvWapYgV4R5CHzptssoag2M7y3Ra7XNta6GPAJPPP5KGB9j14viYXyrzRO5GkX7CRfo8/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.18.2", - "@typescript-eslint/visitor-keys": "8.18.2" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/type-utils": { - "version": "8.18.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.18.2.tgz", - "integrity": "sha512-AB/Wr1Lz31bzHfGm/jgbFR0VB0SML/hd2P1yxzKDM48YmP7vbyJNHRExUE/wZsQj2wUCvbWH8poNHFuxLqCTnA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/typescript-estree": "8.18.2", - "@typescript-eslint/utils": "8.18.2", - "debug": "^4.3.4", - "ts-api-utils": "^1.3.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.8.0" - } - }, - "node_modules/@typescript-eslint/types": { - "version": "8.18.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.18.2.tgz", - "integrity": "sha512-Z/zblEPp8cIvmEn6+tPDIHUbRu/0z5lqZ+NvolL5SvXWT5rQy7+Nch83M0++XzO0XrWRFWECgOAyE8bsJTl1GQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.18.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.18.2.tgz", - "integrity": "sha512-WXAVt595HjpmlfH4crSdM/1bcsqh+1weFRWIa9XMTx/XHZ9TCKMcr725tLYqWOgzKdeDrqVHxFotrvWcEsk2Tg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.18.2", - "@typescript-eslint/visitor-keys": "8.18.2", - "debug": "^4.3.4", - "fast-glob": "^3.3.2", - "is-glob": "^4.0.3", - "minimatch": "^9.0.4", - "semver": "^7.6.0", - "ts-api-utils": "^1.3.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <5.8.0" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@typescript-eslint/utils": { - "version": "8.18.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.18.2.tgz", - "integrity": "sha512-Cr4A0H7DtVIPkauj4sTSXVl+VBWewE9/o40KcF3TV9aqDEOWoXF3/+oRXNby3DYzZeCATvbdksYsGZzplwnK/Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "8.18.2", - "@typescript-eslint/types": "8.18.2", - "@typescript-eslint/typescript-estree": "8.18.2" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.8.0" - } - }, - "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.18.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.18.2.tgz", - "integrity": "sha512-zORcwn4C3trOWiCqFQP1x6G3xTRyZ1LYydnj51cRnJ6hxBlr/cKPckk+PKPUw/fXmvfKTcw7bwY3w9izgx5jZw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.18.2", - "eslint-visitor-keys": "^4.2.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/acorn": { - "version": "8.14.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", - "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", - "dev": true, - "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/acorn-typescript": { - "version": "1.4.13", - "resolved": "https://registry.npmjs.org/acorn-typescript/-/acorn-typescript-1.4.13.tgz", - "integrity": "sha512-xsc9Xv0xlVfwp2o7sQ+GCQ1PgbkdcpWdTzrwXxO3xDMTAywVS3oXVOcOHuRjAPkS4P9b+yc/qNF15460v+jp4Q==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "acorn": ">=8.9.0" - } - }, - "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, - "license": "Python-2.0" - }, - "node_modules/aria-query": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", - "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "license": "MIT" - }, - "node_modules/axios": { - "version": "1.7.9", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.9.tgz", - "integrity": "sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==", - "license": "MIT", - "dependencies": { - "follow-redirects": "^1.15.6", - "form-data": "^4.0.0", - "proxy-from-env": "^1.1.0" - } - }, - "node_modules/axobject-query": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", - "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/braces": { - "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" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/chokidar": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", - "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", - "dev": true, - "license": "MIT", - "dependencies": { - "readdirp": "^4.0.1" - }, - "engines": { - "node": ">= 14.16.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/clsx": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", - "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "license": "MIT", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/commondir": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", - "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", - "dev": true, - "license": "MIT" - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true, - "license": "MIT" - }, - "node_modules/cookie": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", - "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/cssesc": { - "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" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/debug": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", - "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/deep-is": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/deepmerge": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", - "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/devalue": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.1.1.tgz", - "integrity": "sha512-maua5KUiapvEwiEAe+XnlZ3Rh0GD+qI1J/nb9vrJc3muPXvcF/8gXYTWF76+5DAqHyDUtOIImEuo0YKE9mshVw==", - "dev": true, - "license": "MIT" - }, - "node_modules/esbuild": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", - "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=12" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.21.5", - "@esbuild/android-arm": "0.21.5", - "@esbuild/android-arm64": "0.21.5", - "@esbuild/android-x64": "0.21.5", - "@esbuild/darwin-arm64": "0.21.5", - "@esbuild/darwin-x64": "0.21.5", - "@esbuild/freebsd-arm64": "0.21.5", - "@esbuild/freebsd-x64": "0.21.5", - "@esbuild/linux-arm": "0.21.5", - "@esbuild/linux-arm64": "0.21.5", - "@esbuild/linux-ia32": "0.21.5", - "@esbuild/linux-loong64": "0.21.5", - "@esbuild/linux-mips64el": "0.21.5", - "@esbuild/linux-ppc64": "0.21.5", - "@esbuild/linux-riscv64": "0.21.5", - "@esbuild/linux-s390x": "0.21.5", - "@esbuild/linux-x64": "0.21.5", - "@esbuild/netbsd-x64": "0.21.5", - "@esbuild/openbsd-x64": "0.21.5", - "@esbuild/sunos-x64": "0.21.5", - "@esbuild/win32-arm64": "0.21.5", - "@esbuild/win32-ia32": "0.21.5", - "@esbuild/win32-x64": "0.21.5" - } - }, - "node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint": { - "version": "9.17.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.17.0.tgz", - "integrity": "sha512-evtlNcpJg+cZLcnVKwsai8fExnqjGPicK7gnUtlNuzu+Fv9bI0aLpND5T44VLQtoMEnI57LoXO9XAkIXwohKrA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.19.0", - "@eslint/core": "^0.9.0", - "@eslint/eslintrc": "^3.2.0", - "@eslint/js": "9.17.0", - "@eslint/plugin-kit": "^0.2.3", - "@humanfs/node": "^0.16.6", - "@humanwhocodes/module-importer": "^1.0.1", - "@humanwhocodes/retry": "^0.4.1", - "@types/estree": "^1.0.6", - "@types/json-schema": "^7.0.15", - "ajv": "^6.12.4", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.6", - "debug": "^4.3.2", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^8.2.0", - "eslint-visitor-keys": "^4.2.0", - "espree": "^10.3.0", - "esquery": "^1.5.0", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^8.0.0", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "ignore": "^5.2.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", - "natural-compare": "^1.4.0", - "optionator": "^0.9.3" - }, - "bin": { - "eslint": "bin/eslint.js" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://eslint.org/donate" - }, - "peerDependencies": { - "jiti": "*" - }, - "peerDependenciesMeta": { - "jiti": { - "optional": true - } - } - }, - "node_modules/eslint-compat-utils": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/eslint-compat-utils/-/eslint-compat-utils-0.5.1.tgz", - "integrity": "sha512-3z3vFexKIEnjHE3zCMRo6fn/e44U7T1khUjg+Hp0ZQMCigh28rALD0nPFBcGZuiLC5rLZa2ubQHDRln09JfU2Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "semver": "^7.5.4" - }, - "engines": { - "node": ">=12" - }, - "peerDependencies": { - "eslint": ">=6.0.0" - } - }, - "node_modules/eslint-config-prettier": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz", - "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==", - "dev": true, - "license": "MIT", - "bin": { - "eslint-config-prettier": "bin/cli.js" - }, - "peerDependencies": { - "eslint": ">=7.0.0" - } - }, - "node_modules/eslint-plugin-svelte": { - "version": "2.46.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-svelte/-/eslint-plugin-svelte-2.46.1.tgz", - "integrity": "sha512-7xYr2o4NID/f9OEYMqxsEQsCsj4KaMy4q5sANaKkAb6/QeCjYFxRmDm2S3YC3A3pl1kyPZ/syOx/i7LcWYSbIw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.4.0", - "@jridgewell/sourcemap-codec": "^1.4.15", - "eslint-compat-utils": "^0.5.1", - "esutils": "^2.0.3", - "known-css-properties": "^0.35.0", - "postcss": "^8.4.38", - "postcss-load-config": "^3.1.4", - "postcss-safe-parser": "^6.0.0", - "postcss-selector-parser": "^6.1.0", - "semver": "^7.6.2", - "svelte-eslint-parser": "^0.43.0" - }, - "engines": { - "node": "^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ota-meshi" - }, - "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0-0 || ^9.0.0-0", - "svelte": "^3.37.0 || ^4.0.0 || ^5.0.0" - }, - "peerDependenciesMeta": { - "svelte": { - "optional": true - } - } - }, - "node_modules/eslint-scope": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.2.0.tgz", - "integrity": "sha512-PHlWUfG6lvPc3yvP5A4PNyBL1W8fkDUccmI21JUu/+GKZBoH/W5u6usENXUrWFRsyoW5ACUjFGgAFQp5gUlb/A==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-visitor-keys": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", - "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/esm-env": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.1.tgz", - "integrity": "sha512-U9JedYYjCnadUlXk7e1Kr+aENQhtUaoaV9+gZm1T8LC/YBAPJx3NSPIAurFOC0U5vrdSevnUJS2/wUVxGwPhng==", - "dev": true, - "license": "MIT" - }, - "node_modules/espree": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz", - "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "acorn": "^8.14.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.2.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/esquery": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", - "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "estraverse": "^5.1.0" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/esrap": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/esrap/-/esrap-1.3.2.tgz", - "integrity": "sha512-C4PXusxYhFT98GjLSmb20k9PREuUdporer50dhzGuJu9IJXktbMddVCMLAERl5dAHyAi73GWWCE4FVHGP1794g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.4.15" - } - }, - "node_modules/esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "estraverse": "^5.2.0" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estree-walker": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", - "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", - "dev": true, - "license": "MIT" - }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-glob": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", - "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.4" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "node_modules/fast-glob/node_modules/glob-parent": { - "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" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fastq": { - "version": "1.18.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.18.0.tgz", - "integrity": "sha512-QKHXPW0hD8g4UET03SdOdunzSouc9N4AuHdsX8XNcTsuz+yYFILVNIX4l9yHABMhiEI9Db0JTTIpu0wB+Y1QQw==", - "dev": true, - "license": "ISC", - "dependencies": { - "reusify": "^1.0.4" - } - }, - "node_modules/fdir": { - "version": "6.4.2", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.2.tgz", - "integrity": "sha512-KnhMXsKSPZlAhp7+IjUkRZKPb4fUyccpDrdFXbi4QL1qkmFh9kVY09Yox+n4MaOb3lHZ1Tv829C3oaaXoMYPDQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/file-entry-cache": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", - "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "flat-cache": "^4.0.0" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/fill-range": { - "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" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dev": true, - "license": "MIT", - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/flat-cache": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", - "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", - "dev": true, - "license": "MIT", - "dependencies": { - "flatted": "^3.2.9", - "keyv": "^4.5.4" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/flatted": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.2.tgz", - "integrity": "sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA==", - "dev": true, - "license": "ISC" - }, - "node_modules/follow-redirects": { - "version": "1.15.9", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", - "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], - "license": "MIT", - "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } - } - }, - "node_modules/form-data": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz", - "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==", - "license": "MIT", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/glob-parent": { - "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" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/globals": { - "version": "15.14.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-15.14.0.tgz", - "integrity": "sha512-OkToC372DtlQeje9/zHIo5CT8lRP/FUgEOKBEhU4e0abL7J7CD24fD9ohiLN5hagG/kWCYj4K5oaxxtj2Z0Dig==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/globalyzer": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/globalyzer/-/globalyzer-0.1.0.tgz", - "integrity": "sha512-40oNTM9UfG6aBmuKxk/giHn5nQ8RVz/SS4Ir6zgzOv9/qC3kKZ9v4etGTcJbEl/NyVQH7FGU7d+X1egr57Md2Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/globrex": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz", - "integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==", - "dev": true, - "license": "MIT" - }, - "node_modules/graphemer": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", - "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", - "dev": true, - "license": "MIT" - }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/ignore": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/import-fresh": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", - "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", - "dev": true, - "license": "MIT", - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/import-meta-resolve": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.1.0.tgz", - "integrity": "sha512-I6fiaX09Xivtk+THaMfAwnA3MVA5Big1WHF1Dfx9hFuvNIWpXnorlkzhcQf6ehrqQiiZECRt1poOAkPmer3ruw==", - "dev": true, - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.19" - } - }, - "node_modules/is-core-module": { - "version": "2.16.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", - "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", - "dev": true, - "license": "MIT", - "dependencies": { - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-extglob": { - "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" - } - }, - "node_modules/is-glob": { - "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" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-module": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", - "integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==", - "dev": true, - "license": "MIT" - }, - "node_modules/is-number": { - "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" - } - }, - "node_modules/is-reference": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz", - "integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.6" - } - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, - "license": "ISC" - }, - "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/json-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/keyv": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", - "dev": true, - "license": "MIT", - "dependencies": { - "json-buffer": "3.0.1" - } - }, - "node_modules/kleur": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", - "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/known-css-properties": { - "version": "0.35.0", - "resolved": "https://registry.npmjs.org/known-css-properties/-/known-css-properties-0.35.0.tgz", - "integrity": "sha512-a/RAk2BfKk+WFGhhOCAYqSiFLc34k8Mt/6NWRI4joER0EYUzXIcFivjjnoD3+XU1DggLn/tZc3DOAgke7l8a4A==", - "dev": true, - "license": "MIT" - }, - "node_modules/levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/lilconfig": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", - "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "node_modules/locate-character": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz", - "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==", - "dev": true, - "license": "MIT" - }, - "node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/magic-string": { - "version": "0.30.17", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", - "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0" - } - }, - "node_modules/merge2": { - "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" - } - }, - "node_modules/micromatch": { - "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", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/micromatch/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/mri": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", - "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/mrmime": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.0.tgz", - "integrity": "sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, - "license": "MIT" - }, - "node_modules/nanoid": { - "version": "3.3.8", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", - "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true, - "license": "MIT" - }, - "node_modules/optionator": { - "version": "0.9.4", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", - "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", - "dev": true, - "license": "MIT", - "dependencies": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.5" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-limit": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, - "license": "MIT", - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true, - "license": "MIT" - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, - "license": "ISC" - }, - "node_modules/picomatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", - "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/postcss": { - "version": "8.4.49", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz", - "integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "nanoid": "^3.3.7", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/postcss-load-config": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-3.1.4.tgz", - "integrity": "sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==", - "dev": true, - "license": "MIT", - "dependencies": { - "lilconfig": "^2.0.5", - "yaml": "^1.10.2" - }, - "engines": { - "node": ">= 10" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - "peerDependencies": { - "postcss": ">=8.0.9", - "ts-node": ">=9.0.0" - }, - "peerDependenciesMeta": { - "postcss": { - "optional": true - }, - "ts-node": { - "optional": true - } - } - }, - "node_modules/postcss-safe-parser": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/postcss-safe-parser/-/postcss-safe-parser-6.0.0.tgz", - "integrity": "sha512-FARHN8pwH+WiS2OPCxJI8FuRJpTVnn6ZNFiqAM2aeW2LwTHWWmWgIyKC6cUo0L8aeKiF/14MNvnpls6R2PBeMQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - "peerDependencies": { - "postcss": "^8.3.3" - } - }, - "node_modules/postcss-scss": { - "version": "4.0.9", - "resolved": "https://registry.npmjs.org/postcss-scss/-/postcss-scss-4.0.9.tgz", - "integrity": "sha512-AjKOeiwAitL/MXxQW2DliT28EKukvvbEWx3LBmJIRN8KfBGZbRTxNYW0kSqi1COiTZ57nZ9NW06S6ux//N1c9A==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss-scss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "engines": { - "node": ">=12.0" - }, - "peerDependencies": { - "postcss": "^8.4.29" - } - }, - "node_modules/postcss-selector-parser": { - "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", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/prelude-ls": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/prettier": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.4.2.tgz", - "integrity": "sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ==", - "dev": true, - "license": "MIT", - "bin": { - "prettier": "bin/prettier.cjs" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/prettier/prettier?sponsor=1" - } - }, - "node_modules/prettier-plugin-svelte": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/prettier-plugin-svelte/-/prettier-plugin-svelte-3.3.2.tgz", - "integrity": "sha512-kRPjH8wSj2iu+dO+XaUv4vD8qr5mdDmlak3IT/7AOgGIMRG86z/EHOLauFcClKEnOUf4A4nOA7sre5KrJD4Raw==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "prettier": "^3.0.0", - "svelte": "^3.2.0 || ^4.0.0-next.0 || ^5.0.0-next.0" - } - }, - "node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", - "license": "MIT" - }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/queue-microtask": { - "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", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/readdirp": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.0.2.tgz", - "integrity": "sha512-yDMz9g+VaZkqBYS/ozoBJwaBhTbZo3UNYQHNRw1D3UFQB8oHB4uS/tAODO+ZLjGWmUbKnIlOWO+aaIiAxrUWHA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 14.16.0" - }, - "funding": { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/resolve": { - "version": "1.22.10", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", - "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-core-module": "^2.16.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/reusify": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", - "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", - "dev": true, - "license": "MIT", - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, - "node_modules/rollup": { - "version": "4.29.1", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.29.1.tgz", - "integrity": "sha512-RaJ45M/kmJUzSWDs1Nnd5DdV4eerC98idtUOVr6FfKcgxqvjwHmxc5upLF9qZU9EpsVzzhleFahrT3shLuJzIw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "1.0.6" - }, - "bin": { - "rollup": "dist/bin/rollup" - }, - "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" - }, - "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.29.1", - "@rollup/rollup-android-arm64": "4.29.1", - "@rollup/rollup-darwin-arm64": "4.29.1", - "@rollup/rollup-darwin-x64": "4.29.1", - "@rollup/rollup-freebsd-arm64": "4.29.1", - "@rollup/rollup-freebsd-x64": "4.29.1", - "@rollup/rollup-linux-arm-gnueabihf": "4.29.1", - "@rollup/rollup-linux-arm-musleabihf": "4.29.1", - "@rollup/rollup-linux-arm64-gnu": "4.29.1", - "@rollup/rollup-linux-arm64-musl": "4.29.1", - "@rollup/rollup-linux-loongarch64-gnu": "4.29.1", - "@rollup/rollup-linux-powerpc64le-gnu": "4.29.1", - "@rollup/rollup-linux-riscv64-gnu": "4.29.1", - "@rollup/rollup-linux-s390x-gnu": "4.29.1", - "@rollup/rollup-linux-x64-gnu": "4.29.1", - "@rollup/rollup-linux-x64-musl": "4.29.1", - "@rollup/rollup-win32-arm64-msvc": "4.29.1", - "@rollup/rollup-win32-ia32-msvc": "4.29.1", - "@rollup/rollup-win32-x64-msvc": "4.29.1", - "fsevents": "~2.3.2" - } - }, - "node_modules/run-parallel": { - "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", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "queue-microtask": "^1.2.2" - } - }, - "node_modules/sade": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz", - "integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==", - "dev": true, - "license": "MIT", - "dependencies": { - "mri": "^1.1.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/semver": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", - "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/set-cookie-parser": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz", - "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/sirv": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.0.tgz", - "integrity": "sha512-BPwJGUeDaDCHihkORDchNyyTvWFhcusy1XMmhEVTQTwGeybFbp8YEmB+njbPnth1FibULBSBVwCQni25XlCUDg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@polka/url": "^1.0.0-next.24", - "mrmime": "^2.0.0", - "totalist": "^3.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/source-map-js": { - "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" - } - }, - "node_modules/strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/svelte": { - "version": "5.16.0", - "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.16.0.tgz", - "integrity": "sha512-Ygqsiac6UogVED2ruKclU+pOeMThxWtp9LG+li7BXeDKC2paVIsRTMkNmcON4Zejerd1s5sZHWx6ZtU85xklVg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@ampproject/remapping": "^2.3.0", - "@jridgewell/sourcemap-codec": "^1.5.0", - "@types/estree": "^1.0.5", - "acorn": "^8.12.1", - "acorn-typescript": "^1.4.13", - "aria-query": "^5.3.1", - "axobject-query": "^4.1.0", - "clsx": "^2.1.1", - "esm-env": "^1.2.1", - "esrap": "^1.3.2", - "is-reference": "^3.0.3", - "locate-character": "^3.0.0", - "magic-string": "^0.30.11", - "zimmerframe": "^1.1.2" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/svelte-check": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.1.1.tgz", - "integrity": "sha512-NfaX+6Qtc8W/CyVGS/F7/XdiSSyXz+WGYA9ZWV3z8tso14V2vzjfXviKaTFEzB7g8TqfgO2FOzP6XT4ApSTUTw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.25", - "chokidar": "^4.0.1", - "fdir": "^6.2.0", - "picocolors": "^1.0.0", - "sade": "^1.7.4" - }, - "bin": { - "svelte-check": "bin/svelte-check" - }, - "engines": { - "node": ">= 18.0.0" - }, - "peerDependencies": { - "svelte": "^4.0.0 || ^5.0.0-next.0", - "typescript": ">=5.0.0" - } - }, - "node_modules/svelte-eslint-parser": { - "version": "0.43.0", - "resolved": "https://registry.npmjs.org/svelte-eslint-parser/-/svelte-eslint-parser-0.43.0.tgz", - "integrity": "sha512-GpU52uPKKcVnh8tKN5P4UZpJ/fUDndmq7wfsvoVXsyP+aY0anol7Yqo01fyrlaWGMFfm4av5DyrjlaXdLRJvGA==", - "dev": true, - "license": "MIT", - "dependencies": { - "eslint-scope": "^7.2.2", - "eslint-visitor-keys": "^3.4.3", - "espree": "^9.6.1", - "postcss": "^8.4.39", - "postcss-scss": "^4.0.9" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ota-meshi" - }, - "peerDependencies": { - "svelte": "^3.37.0 || ^4.0.0 || ^5.0.0" - }, - "peerDependenciesMeta": { - "svelte": { - "optional": true - } - } - }, - "node_modules/svelte-eslint-parser/node_modules/eslint-scope": { - "version": "7.2.2", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", - "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/svelte-eslint-parser/node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/svelte-eslint-parser/node_modules/espree": { - "version": "9.6.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", - "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "acorn": "^8.9.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.4.1" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/tiny-glob": { - "version": "0.2.9", - "resolved": "https://registry.npmjs.org/tiny-glob/-/tiny-glob-0.2.9.tgz", - "integrity": "sha512-g/55ssRPUjShh+xkfx9UPDXqhckHEsHr4Vd9zX55oSdGZc/MD0m3sferOkwWtp98bv+kcVfEHtRJgBVJzelrzg==", - "dev": true, - "license": "MIT", - "dependencies": { - "globalyzer": "0.1.0", - "globrex": "^0.1.2" - } - }, - "node_modules/to-regex-range": { - "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" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/totalist": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", - "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/ts-api-utils": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.3.tgz", - "integrity": "sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=16" - }, - "peerDependencies": { - "typescript": ">=4.2.0" - } - }, - "node_modules/type-check": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/typescript": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz", - "integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/typescript-eslint": { - "version": "8.18.2", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.18.2.tgz", - "integrity": "sha512-KuXezG6jHkvC3MvizeXgupZzaG5wjhU3yE8E7e6viOvAvD9xAWYp8/vy0WULTGe9DYDWcQu7aW03YIV3mSitrQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/eslint-plugin": "8.18.2", - "@typescript-eslint/parser": "8.18.2", - "@typescript-eslint/utils": "8.18.2" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.8.0" - } - }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/util-deprecate": { - "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/vite": { - "version": "5.4.11", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.11.tgz", - "integrity": "sha512-c7jFQRklXua0mTzneGW9QVyxFjUgwcihC4bXEtujIo2ouWCe1Ajt/amn2PCxYnhYfd5k09JX3SB7OYWFKYqj8Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "esbuild": "^0.21.3", - "postcss": "^8.4.43", - "rollup": "^4.20.0" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^18.0.0 || >=20.0.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - }, - "peerDependencies": { - "@types/node": "^18.0.0 || >=20.0.0", - "less": "*", - "lightningcss": "^1.21.0", - "sass": "*", - "sass-embedded": "*", - "stylus": "*", - "sugarss": "*", - "terser": "^5.4.0" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "less": { - "optional": true - }, - "lightningcss": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - } - } - }, - "node_modules/vitefu": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.0.4.tgz", - "integrity": "sha512-y6zEE3PQf6uu/Mt6DTJ9ih+kyJLr4XcSgHR2zUkM8SWDhuixEJxfJ6CZGMHh1Ec3vPLoEA0IHU5oWzVqw8ulow==", - "dev": true, - "license": "MIT", - "workspaces": [ - "tests/deps/*", - "tests/projects/*" - ], - "peerDependencies": { - "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0" - }, - "peerDependenciesMeta": { - "vite": { - "optional": true - } - } - }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/word-wrap": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", - "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/yaml": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", - "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">= 6" - } - }, - "node_modules/yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/zimmerframe": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.2.tgz", - "integrity": "sha512-rAbqEGa8ovJy4pyBxZM70hg4pE6gDgaQ0Sl9M3enG3I0d6H4XSAM3GeNGLKnsBpuijUow064sf7ww1nutC5/3w==", - "dev": true, - "license": "MIT" - } - } -} diff --git a/frontend/package.json b/frontend/package.json deleted file mode 100644 index b1ef6c7..0000000 --- a/frontend/package.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "name": "formies-fe", - "private": true, - "version": "0.0.1", - "type": "module", - "scripts": { - "dev": "vite dev", - "build": "vite build", - "preview": "vite preview", - "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", - "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", - "format": "prettier --write .", - "lint": "prettier --check . && eslint ." - }, - "devDependencies": { - "@eslint/compat": "^1.2.3", - "@sveltejs/adapter-auto": "^3.0.0", - "@sveltejs/adapter-node": "^5.2.11", - "@sveltejs/kit": "^2.0.0", - "@sveltejs/vite-plugin-svelte": "^4.0.0", - "eslint": "^9.7.0", - "eslint-config-prettier": "^9.1.0", - "eslint-plugin-svelte": "^2.36.0", - "globals": "^15.0.0", - "prettier": "^3.3.2", - "prettier-plugin-svelte": "^3.2.6", - "svelte": "^5.0.0", - "svelte-check": "^4.0.0", - "typescript": "^5.0.0", - "typescript-eslint": "^8.0.0", - "vite": "^5.4.11" - }, - "dependencies": { - "@types/uuid": "^10.0.0", - "axios": "^1.7.9" - } -} diff --git a/frontend/src/app.css b/frontend/src/app.css deleted file mode 100644 index 1435e65..0000000 --- a/frontend/src/app.css +++ /dev/null @@ -1,188 +0,0 @@ -/* Reset and base styles */ -:root { - --primary-color: #4a90e2; - --secondary-color: #f5f5f5; - --border-color: #ddd; - --text-color: #333; - --error-color: #e74c3c; - --success-color: #2ecc71; - --shadow: 0 2px 4px rgba(0, 0, 0, 0.1); -} - -* { - box-sizing: border-box; - margin: 0; - padding: 0; -} - -body { - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, - sans-serif; - line-height: 1.6; - color: var(--text-color); - background-color: #fff; - padding: 2rem; -} - -/* Typography */ -h1 { - font-size: 2rem; - margin-bottom: 1.5rem; - color: var(--text-color); -} - -h2 { - font-size: 1.5rem; - margin: 1.5rem 0 1rem; -} - -/* Links */ -a { - color: var(--primary-color); - text-decoration: none; - transition: color 0.2s; -} - -a:hover { - color: #357abd; - text-decoration: underline; -} - -/* Lists */ -ul { - list-style: none; - margin: 1rem 0; -} - -li { - padding: 0.75rem; - border-bottom: 1px solid var(--border-color); -} - -li:last-child { - border-bottom: none; -} - -/* Forms */ -form { - max-width: 800px; - margin: 2rem 0; -} - -.form-group { - margin-bottom: 1.5rem; -} - -label { - display: block; - margin-bottom: 0.5rem; - font-weight: 500; -} - -input[type='text'], -input[type='number'], -input[type='date'], -select, -textarea { - width: 100%; - padding: 0.75rem; - border: 1px solid var(--border-color); - border-radius: 4px; - font-size: 1rem; - margin-bottom: 1rem; - transition: border-color 0.2s; -} - -input:focus, -select:focus, -textarea:focus { - outline: none; - border-color: var(--primary-color); - box-shadow: 0 0 0 2px rgba(74, 144, 226, 0.2); -} - -textarea { - min-height: 100px; - resize: vertical; -} - -/* Buttons */ -button { - background-color: var(--primary-color); - color: white; - border: none; - padding: 0.75rem 1.5rem; - border-radius: 4px; - cursor: pointer; - font-size: 1rem; - transition: background-color 0.2s; -} - -button:hover:not(:disabled) { - background-color: #357abd; -} - -button:disabled { - background-color: #ccc; - cursor: not-allowed; -} - -button.secondary { - background-color: var(--secondary-color); - color: var(--text-color); - border: 1px solid var(--border-color); -} - -button.secondary:hover:not(:disabled) { - background-color: #e8e8e8; -} - -button + button { - margin-left: 1rem; -} - -/* Field management */ -.field-container { - background-color: var(--secondary-color); - padding: 1rem; - border-radius: 4px; - margin-bottom: 1rem; -} - -/* Submissions */ -.submissions-list { - background-color: var(--secondary-color); - padding: 1rem; - border-radius: 4px; -} - -.submission-item { - background-color: white; - padding: 1rem; - margin-bottom: 0.5rem; - border-radius: 4px; - box-shadow: var(--shadow); -} - -/* Utility classes */ -.container { - max-width: 1200px; - margin: 0 auto; - padding: 0 1rem; -} - -.loading { - text-align: center; - padding: 2rem; - color: #666; -} - -.error { - color: var(--error-color); - margin: 1rem 0; -} - -.success { - color: var(--success-color); - margin: 1rem 0; -} diff --git a/frontend/src/app.d.ts b/frontend/src/app.d.ts deleted file mode 100644 index da08e6d..0000000 --- a/frontend/src/app.d.ts +++ /dev/null @@ -1,13 +0,0 @@ -// See https://svelte.dev/docs/kit/types#app.d.ts -// for information about these interfaces -declare global { - namespace App { - // interface Error {} - // interface Locals {} - // interface PageData {} - // interface PageState {} - // interface Platform {} - } -} - -export {}; diff --git a/frontend/src/app.html b/frontend/src/app.html deleted file mode 100644 index 77a5ff5..0000000 --- a/frontend/src/app.html +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - %sveltekit.head% - - -
%sveltekit.body%
- - diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts deleted file mode 100644 index d2e2930..0000000 --- a/frontend/src/lib/api.ts +++ /dev/null @@ -1,176 +0,0 @@ -const API_BASE_URL = 'http://127.0.0.1:8080'; - -// A simple function to retrieve the token from local storage or wherever it is stored -function getAuthToken(): string | null { - return localStorage.getItem('auth_token'); // Assuming the token is stored in localStorage -} - -// A simple function to save the token -function setAuthToken(token: string): void { - localStorage.setItem('auth_token', token); -} - -// A simple function to save the token -function delAuthToken(): void { - localStorage.removeItem('auth_token'); -} - -// A simple function to retrieve the token from local storage or wherever it is stored -function getAuthToken(): string | null { - return localStorage.getItem('auth_token'); // Assuming the token is stored in localStorage -} - -// A simple function to save the token -function setAuthToken(token: string): void { - localStorage.setItem('auth_token', token); -} - -// A simple function to save the token -function delAuthToken(): void { - localStorage.removeItem('auth_token'); -} - -/** - * Helper to make authenticated requests. - * @param endpoint The API endpoint (relative to base URL). - * @param options Fetch options such as method, headers, and body. - * @returns The JSON-parsed response. - */ -// eslint-disable-next-line @typescript-eslint/no-explicit-any -async function authenticatedRequest(endpoint: string, options: RequestInit): Promise { - const token = localStorage.getItem('authToken'); // Replace with a secure token storage solution if needed - if (!token) { - throw new Error('Authentication token is missing. Please log in.'); - } - - const response = await fetch(`${API_BASE_URL}${endpoint}`, { - ...options, - headers: { - ...options.headers, - Authorization: `Bearer ${token}`, // Include the token in the Authorization header - 'Content-Type': 'application/json' - } - }); - - if (!response.ok) { - throw new Error(`Error: ${response.statusText}`); - } - - return response.json(); -} - -/** - * Create a new form (authenticated). - * @param name The name of the form. - * @param fields The fields of the form in JSON format. - * @returns The ID of the created form. - */ -export async function createForm(name: string, fields: unknown): Promise { - const token = getAuthToken(); // Get the token from storage - const response = await fetch(`${API_BASE_URL}/forms`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${token}` // Add token to the headers - }, - body: JSON.stringify({ name, fields }) - }); -} - -/** - * Get all forms (authenticated). - * @returns An array of forms. - */ -export async function getForms(): Promise { - const token = getAuthToken(); // Get the token from storage - const response = await fetch(`${API_BASE_URL}/forms`, { - method: 'GET', - headers: { - Accept: 'application/json', - Authorization: `Bearer ${token}` // Add token to the headers - } - }); - - if (!response.ok) { - throw new Error(`Error fetching forms: ${response.statusText}`); - } - - return await response.json(); -} - -/** - * Submit a form (unauthenticated). - * @param formId The ID of the form to submit. - * @param data The submission data in JSON format. - * @returns The ID of the created submission. - */ -export async function submitForm(formId: string, data: unknown): Promise { - const token = getAuthToken(); // Get the token from storage - const response = await fetch(`${API_BASE_URL}/forms/${formId}/submissions`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${token}` // Add token to the headers - }, - body: JSON.stringify(data) - }); - - if (!response.ok) { - throw new Error(`Error submitting form: ${response.statusText}`); - } - - return await response.json(); -} - -/** - * Admin login to get a token. - * @param credentials The login credentials (username and password). - * @returns The generated JWT token if successful. - */ -export async function getSubmissions(formId: string): Promise { - const token = getAuthToken(); // Get the token from storage - const response = await fetch(`${API_BASE_URL}/forms/${formId}/submissions`, { - method: 'GET', - headers: { - Accept: 'application/json', - Authorization: `Bearer ${token}` // Add token to the headers - } - }); - - if (!response.ok) { - throw new Error(`Error fetching submissions: ${response.statusText}`); - } - - return await response.json(); -} - -/** - * Login and get the authentication token. - * @param username The username. - * @param password The password. - * @returns The authentication token. - */ -export async function login(username: string, password: string): Promise { - const response = await fetch(`${API_BASE_URL}/login`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ username, password }) - }); - - if (!response.ok) { - throw new Error(`Error logging in: ${response.statusText}`); - } - - const { token } = await response.json(); - setAuthToken(token); // Store the token in localStorage -} - -export function logout() { - delAuthToken(); -} - -export function loggedIn() { - return localStorage.getItem('auth_token') !== null; -} diff --git a/frontend/src/lib/components/Dashboard.svelte b/frontend/src/lib/components/Dashboard.svelte deleted file mode 100644 index ccecf39..0000000 --- a/frontend/src/lib/components/Dashboard.svelte +++ /dev/null @@ -1,32 +0,0 @@ - - -
-

Forms Dashboard

- {#if forms.length === 0} -

No forms available.

- {/if} - -
    - {#each forms as form} -
  • -

    {form.name}

    - -
  • - {/each} -
-
diff --git a/frontend/src/lib/components/FormBuilder.svelte b/frontend/src/lib/components/FormBuilder.svelte deleted file mode 100644 index 87fdc45..0000000 --- a/frontend/src/lib/components/FormBuilder.svelte +++ /dev/null @@ -1,44 +0,0 @@ - - -
-

Create a Form

- - - - - - {#each fields as field, index} -
- - - {field.type} - -
- {/each} - - -
diff --git a/frontend/src/lib/components/FormRenderer.svelte b/frontend/src/lib/components/FormRenderer.svelte deleted file mode 100644 index 32e23f8..0000000 --- a/frontend/src/lib/components/FormRenderer.svelte +++ /dev/null @@ -1,35 +0,0 @@ - - -
- {#each form.fields as field} -
- - - {#if field.type === 'text'} - - {:else if field.type === 'number'} - - {/if} -
- {/each} - - -
diff --git a/frontend/src/lib/components/Navbar.svelte b/frontend/src/lib/components/Navbar.svelte deleted file mode 100644 index 7036c68..0000000 --- a/frontend/src/lib/components/Navbar.svelte +++ /dev/null @@ -1,11 +0,0 @@ - - - diff --git a/frontend/src/lib/components/Routes.svelte b/frontend/src/lib/components/Routes.svelte deleted file mode 100644 index 599be9b..0000000 --- a/frontend/src/lib/components/Routes.svelte +++ /dev/null @@ -1,32 +0,0 @@ - - -
-

Forms Dashboard

- {#if forms.length === 0} -

No forms available.

- {/if} - -
    - {#each forms as form} -
  • -

    {form.name}

    - -
  • - {/each} -
-
diff --git a/frontend/src/lib/session.svelte.ts b/frontend/src/lib/session.svelte.ts deleted file mode 100644 index c035516..0000000 --- a/frontend/src/lib/session.svelte.ts +++ /dev/null @@ -1,17 +0,0 @@ -import type { AdminUser } from './types'; - -const key2 = 'username'; - -function login(user: AdminUser) { - localStorage.setItem(key2, user.username); -} - -function logout() { - localStorage.removeItem(key2); -} - -function loggedIn() { - return localStorage.getItem('authToken') !== null; -} - -export default { login, logout, loggedIn }; diff --git a/frontend/src/lib/types.ts b/frontend/src/lib/types.ts deleted file mode 100644 index 072609f..0000000 --- a/frontend/src/lib/types.ts +++ /dev/null @@ -1,29 +0,0 @@ -export interface FormField { - label: string; - name: string; - field_type: 'text' | 'number' | 'date' | 'textarea'; -} - -export interface Form { - id: string; - name: string; - fields: FormField[]; - created_at?: string; -} - -export interface Submission { - id: string; - form_id: string; - data: Record; - created_at?: string; -} - -export interface LoginCredentials { - username: string; - password: string; -} - -export interface AdminUser { - username: string; - password_hash: string; -} diff --git a/frontend/src/routes/(auth)/+layout.svelte b/frontend/src/routes/(auth)/+layout.svelte deleted file mode 100644 index d6bb38e..0000000 --- a/frontend/src/routes/(auth)/+layout.svelte +++ /dev/null @@ -1,7 +0,0 @@ - - - -{@render children()} diff --git a/frontend/src/routes/(auth)/+layout.ts b/frontend/src/routes/(auth)/+layout.ts deleted file mode 100644 index fe1e106..0000000 --- a/frontend/src/routes/(auth)/+layout.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { loggedIn } from '$lib/api'; -import { redirect } from '@sveltejs/kit'; - -export async function load() { - if (!loggedIn()) { - redirect(307, '/login'); - } -} diff --git a/frontend/src/routes/(auth)/create/+page.svelte b/frontend/src/routes/(auth)/create/+page.svelte deleted file mode 100644 index 89f713d..0000000 --- a/frontend/src/routes/(auth)/create/+page.svelte +++ /dev/null @@ -1,62 +0,0 @@ - - -
-

Create Form

-
-
- - -
- -

Fields

- {#each fields as field, i} -
-
- - -
-
- - -
-
- - -
- -
- {/each} - - -
-
diff --git a/frontend/src/routes/(auth)/form/[id]/+page.svelte b/frontend/src/routes/(auth)/form/[id]/+page.svelte deleted file mode 100644 index f9ddcd4..0000000 --- a/frontend/src/routes/(auth)/form/[id]/+page.svelte +++ /dev/null @@ -1,61 +0,0 @@ - - -
-

{form?.name}

- {#if form} -
- {#each form.fields as field} -
- - {#if field.field_type === 'text'} - - {:else if field.field_type === 'number'} - - {:else if field.field_type === 'date'} - - {:else if field.field_type === 'textarea'} - - {/if} -
- {/each} - -
-

Submissions

-
- {#each submissions as submission} -
- {JSON.stringify(submission.data)} -
- {/each} -
- {:else} -

Loading...

- {/if} -
diff --git a/frontend/src/routes/(auth)/form/[id]/+page.ts b/frontend/src/routes/(auth)/form/[id]/+page.ts deleted file mode 100644 index fcbefb4..0000000 --- a/frontend/src/routes/(auth)/form/[id]/+page.ts +++ /dev/null @@ -1,5 +0,0 @@ -export function load({ params }) { - return { - params - }; -} diff --git a/frontend/src/routes/(auth)/main/+page.svelte b/frontend/src/routes/(auth)/main/+page.svelte deleted file mode 100644 index a81338b..0000000 --- a/frontend/src/routes/(auth)/main/+page.svelte +++ /dev/null @@ -1,22 +0,0 @@ - - -
- Create a New Form -
    - {#each forms as form} -
  • - {form.name} -
  • - {/each} -
-
diff --git a/frontend/src/routes/+layout.svelte b/frontend/src/routes/+layout.svelte deleted file mode 100644 index b93e9ba..0000000 --- a/frontend/src/routes/+layout.svelte +++ /dev/null @@ -1,7 +0,0 @@ - - -{@render children()} diff --git a/frontend/src/routes/+layout.ts b/frontend/src/routes/+layout.ts deleted file mode 100644 index a3d1578..0000000 --- a/frontend/src/routes/+layout.ts +++ /dev/null @@ -1 +0,0 @@ -export const ssr = false; diff --git a/frontend/src/routes/+page.ts b/frontend/src/routes/+page.ts deleted file mode 100644 index 471709e..0000000 --- a/frontend/src/routes/+page.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { loggedIn } from '$lib/api'; -import { redirect } from '@sveltejs/kit'; - -export async function load() { - const page = loggedIn() ? '/main' : '/login'; - redirect(307, page); -} diff --git a/frontend/src/routes/login/+page.svelte b/frontend/src/routes/login/+page.svelte deleted file mode 100644 index b69d326..0000000 --- a/frontend/src/routes/login/+page.svelte +++ /dev/null @@ -1,51 +0,0 @@ - - - diff --git a/frontend/src/routes/login/+page.ts b/frontend/src/routes/login/+page.ts deleted file mode 100644 index 22f14b6..0000000 --- a/frontend/src/routes/login/+page.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { loggedIn } from '$lib/api'; -import { redirect } from '@sveltejs/kit'; - -export async function load() { - if (loggedIn()) { - redirect(307, '/'); - } -} diff --git a/frontend/static/favicon.png b/frontend/static/favicon.png deleted file mode 100644 index 825b9e65af7c104cfb07089bb28659393b4f2097..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1571 zcmV+;2Hg3HP)Px)-AP12RCwC$UE6KzI1p6{F2N z1VK2vi|pOpn{~#djwYcWXTI_im_u^TJgMZ4JMOsSj!0ma>B?-(Hr@X&W@|R-$}W@Z zgj#$x=!~7LGqHW?IO8+*oE1MyDp!G=L0#^lUx?;!fXv@l^6SvTnf^ac{5OurzC#ZMYc20lI%HhX816AYVs1T3heS1*WaWH z%;x>)-J}YB5#CLzU@GBR6sXYrD>Vw(Fmt#|JP;+}<#6b63Ike{Fuo!?M{yEffez;| zp!PfsuaC)>h>-AdbnwN13g*1LowNjT5?+lFVd#9$!8Z9HA|$*6dQ8EHLu}U|obW6f z2%uGv?vr=KNq7YYa2Roj;|zooo<)lf=&2yxM@e`kM$CmCR#x>gI>I|*Ubr({5Y^rb zghxQU22N}F51}^yfDSt786oMTc!W&V;d?76)9KXX1 z+6Okem(d}YXmmOiZq$!IPk5t8nnS{%?+vDFz3BevmFNgpIod~R{>@#@5x9zJKEHLHv!gHeK~n)Ld!M8DB|Kfe%~123&Hz1Z(86nU7*G5chmyDe ziV7$pB7pJ=96hpxHv9rCR29%bLOXlKU<_13_M8x)6;P8E1Kz6G<&P?$P^%c!M5`2` zfY2zg;VK5~^>TJGQzc+33-n~gKt{{of8GzUkWmU110IgI0DLxRIM>0US|TsM=L|@F z0Bun8U!cRB7-2apz=y-7*UxOxz@Z0)@QM)9wSGki1AZ38ceG7Q72z5`i;i=J`ILzL z@iUO?SBBG-0cQuo+an4TsLy-g-x;8P4UVwk|D8{W@U1Zi z!M)+jqy@nQ$p?5tsHp-6J304Q={v-B>66$P0IDx&YT(`IcZ~bZfmn11#rXd7<5s}y zBi9eim&zQc0Dk|2>$bs0PnLmDfMP5lcXRY&cvJ=zKxI^f0%-d$tD!`LBf9^jMSYUA zI8U?CWdY@}cRq6{5~y+)#h1!*-HcGW@+gZ4B};0OnC~`xQOyH19z*TA!!BJ%9s0V3F?CAJ{hTd#*tf+ur-W9MOURF-@B77_-OshsY}6 zOXRY=5%C^*26z?l)1=$bz30!so5tfABdSYzO+H=CpV~aaUefmjvfZ3Ttu9W&W3Iu6 zROlh0MFA5h;my}8lB0tAV-Rvc2Zs_CCSJnx@d`**$idgy-iMob4dJWWw|21b4NB=LfsYp0Aeh{Ov)yztQi;eL4y5 zMi>8^SzKqk8~k?UiQK^^-5d8c%bV?$F8%X~czyiaKCI2=UH +This section contains a summary of this file. + + +This file contains a packed representation of the entire repository's contents. +It is designed to be easily consumable by AI systems for analysis, code review, +or other automated processes. + + + +The content is organized as follows: +1. This summary section +2. Repository information +3. Directory structure +4. Repository files, each consisting of: + - File path as an attribute + - Full contents of the file + + + +- This file should be treated as read-only. Any changes should be made to the + original repository files, not this packed version. +- When processing this file, use the file path to distinguish + between different files in the repository. +- Be aware that this file may contain sensitive information. Handle it with + the same level of security as you would the original repository. + + + +- Some files may have been excluded based on .gitignore rules and Repomix's configuration +- Binary files are not included in this packed representation. Please refer to the Repository Structure section for a complete list of file paths, including binary files +- Files matching patterns in .gitignore are excluded +- Files matching default ignore patterns are excluded +- Files are sorted by Git change count (files with more changes are at the bottom) + + + + + + + + + +.gitignore +Cargo.toml +src/auth.rs +src/db.rs +src/handlers.rs +src/main.rs +src/models.rs + + + +This section contains the contents of the repository's files. + + +/target + + + +[package] +name = "formies_be" +version = "0.1.0" +edition = "2021" + +[dependencies] +actix-web = "4.0" +rusqlite = { version = "0.29", features = ["bundled", "chrono"] } +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +uuid = { version = "1.0", features = ["v4"] } +actix-files = "0.6" +actix-cors = "0.6" +env_logger = "0.10" +log = "0.4" +futures = "0.3" +bcrypt = "0.13" +anyhow = "1.0" +dotenv = "0.15.0" +chrono = { version = "0.4", features = ["serde"] } +regex = "1" +url = "2" + + + +// src/auth.rs +use actix_web::error::{ErrorInternalServerError, ErrorUnauthorized}; // Specific error types +use actix_web::{ + dev::Payload, http::header::AUTHORIZATION, web, Error as ActixWebError, FromRequest, + HttpRequest, Result as ActixResult, +}; +use chrono::{Duration, Utc}; // Import chrono for time checks +use futures::future::{ready, Ready}; +use log; // Use the log crate +use rusqlite::Connection; +use std::sync::{Arc, Mutex}; + +// Represents an authenticated user via token +pub struct Auth { + pub user_id: String, +} + +impl FromRequest for Auth { + // Use actix_web::Error for consistency in error handling within Actix + type Error = ActixWebError; + // Use Ready from futures 0.3 + type Future = Ready>; + + fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future { + // Extract database connection pool from application data + // Replace .expect() with proper error handling + let db_data_result = req.app_data::>>>(); + + let db_data = match db_data_result { + Some(data) => data, + None => { + log::error!("Database connection missing in application data configuration."); + return ready(Err(ErrorInternalServerError( + "Internal server error (app configuration)", + ))); + } + }; + + // Extract Authorization header + let auth_header = req.headers().get(AUTHORIZATION); + + if let Some(auth_header_value) = auth_header { + // Convert header value to string + if let Ok(auth_str) = auth_header_value.to_str() { + // Check if it starts with "Bearer " + if auth_str.starts_with("Bearer ") { + // Extract the token part + let token = &auth_str[7..]; + + // Lock the mutex to get access to the connection + // Handle potential mutex poisoning explicitly + let conn_guard = match db_data.lock() { + Ok(guard) => guard, + Err(poisoned) => { + log::error!("Database mutex poisoned: {}", poisoned); + // Return internal server error if mutex is poisoned + return ready(Err(ErrorInternalServerError( + "Internal server error (database lock)", + ))); + } + }; + + // Validate the token against the database (now includes expiration check) + match super::db::validate_token(&conn_guard, token) { + // Token is valid and not expired, return Ok with Auth struct + Ok(Some(user_id)) => { + log::debug!("Token validated successfully for user_id: {}", user_id); + ready(Ok(Auth { user_id })) + } + // Token is invalid, not found, or expired + Ok(None) => { + log::warn!("Invalid or expired token received"); // Avoid logging token + ready(Err(ErrorUnauthorized("Invalid or expired token"))) + } + // Database error during token validation + Err(e) => { + log::error!("Database error during token validation: {:?}", e); + // Return Unauthorized to avoid leaking internal error details + // Consider mapping specific DB errors if needed, but Unauthorized is generally safe + ready(Err(ErrorUnauthorized("Token validation failed"))) + } + } + } else { + // Header present but not "Bearer " format + log::warn!("Invalid Authorization header format (not Bearer)"); + ready(Err(ErrorUnauthorized("Invalid token format"))) + } + } else { + // Header value contains invalid characters + log::warn!("Authorization header contains invalid characters"); + ready(Err(ErrorUnauthorized("Invalid token value"))) + } + } else { + // Authorization header is missing + log::warn!("Missing Authorization header"); + ready(Err(ErrorUnauthorized("Missing authorization token"))) + } + } +} + + + +// src/db.rs +use anyhow::{anyhow, Context, Result as AnyhowResult}; +use bcrypt::{hash, verify, DEFAULT_COST}; +use chrono::{Duration as ChronoDuration, Utc}; // Use Utc for timestamps +use log; // Use the log crate +use rusqlite::{ + params, types::Value as RusqliteValue, Connection, OptionalExtension, Result as RusqliteResult, +}; +use std::env; +use uuid::Uuid; + +use crate::models; + +// Configurable token lifetime (e.g., from environment variable or default) +const TOKEN_LIFETIME_HOURS: i64 = 24; // Default to 24 hours + +// Initialize the database connection and create tables if they don't exist +pub fn init_db(database_url: &str) -> AnyhowResult { + log::info!("Attempting to open or create database at: {}", database_url); + let conn = Connection::open(database_url) + .context(format!("Failed to open the database at {}", database_url))?; + + log::debug!("Creating 'users' table if not exists..."); + conn.execute( + "CREATE TABLE IF NOT EXISTS users ( + id TEXT PRIMARY KEY, + username TEXT NOT NULL UNIQUE, + password TEXT NOT NULL, -- Stores bcrypt hashed password + token TEXT UNIQUE, -- Stores the current session token (UUID) + token_expires_at DATETIME -- Timestamp when the token expires + )", + [], + ) + .context("Failed to create 'users' table")?; + + log::debug!("Creating 'forms' table if not exists..."); + // Storing complex form definitions as JSON blobs in TEXT columns is pragmatic + // but sacrifices DB-level type safety and query capabilities. Ensure robust + // application-level validation and consider backup strategies carefully. + conn.execute( + "CREATE TABLE IF NOT EXISTS forms ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL, + fields TEXT NOT NULL, -- Stores JSON definition of form fields + created_at DATETIME DEFAULT CURRENT_TIMESTAMP + )", + [], + ) + .context("Failed to create 'forms' table")?; + + log::debug!("Creating 'submissions' table if not exists..."); + // Storing submission data as JSON blobs has similar tradeoffs as form fields. + conn.execute( + "CREATE TABLE IF NOT EXISTS submissions ( + id TEXT PRIMARY KEY, + form_id TEXT NOT NULL, + data TEXT NOT NULL, -- Stores JSON submission data + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (form_id) REFERENCES forms (id) ON DELETE CASCADE + )", + [], + ) + .context("Failed to create 'submissions' table")?; + + // Setup the initial admin user if it doesn't exist, using environment variables + setup_initial_admin(&conn).context("Failed to setup initial admin user")?; + + log::info!("Database initialization complete."); + Ok(conn) +} + +// Sets up the initial admin user from *required* environment variables if it doesn't exist +fn setup_initial_admin(conn: &Connection) -> AnyhowResult<()> { + // CRITICAL SECURITY CHANGE: Remove default credentials. Require env vars. + let initial_admin_username = env::var("INITIAL_ADMIN_USERNAME") + .map_err(|_| anyhow!("FATAL: INITIAL_ADMIN_USERNAME environment variable is not set."))?; + let initial_admin_password = env::var("INITIAL_ADMIN_PASSWORD") + .map_err(|_| anyhow!("FATAL: INITIAL_ADMIN_PASSWORD environment variable is not set."))?; + + if initial_admin_username.is_empty() || initial_admin_password.is_empty() { + return Err(anyhow!( + "FATAL: INITIAL_ADMIN_USERNAME and INITIAL_ADMIN_PASSWORD must not be empty." + )); + } + + // Check password complexity? (Optional enhancement) + + add_user_if_not_exists(conn, &initial_admin_username, &initial_admin_password) + .context("Failed during initial admin user setup")?; + Ok(()) +} + +// Adds a user with a hashed password if the username doesn't exist +pub fn add_user_if_not_exists( + conn: &Connection, + username: &str, + password: &str, +) -> AnyhowResult { + // Check if user already exists + let user_exists: bool = conn + .query_row( + "SELECT EXISTS(SELECT 1 FROM users WHERE username = ?1)", + params![username], + |row| row.get::<_, i32>(0), + ) + .context(format!("Failed to check existence of user '{}'", username))? + == 1; + + if user_exists { + log::debug!("User '{}' already exists, skipping creation.", username); + return Ok(false); // User already exists, nothing added + } + + // Generate a UUID for the new user + let user_id = Uuid::new_v4().to_string(); + + // Hash the password using bcrypt + // Ensure the cost factor is appropriate for your security needs and hardware. + // Higher cost means slower hashing and verification, but better resistance to brute-force. + log::debug!( + "Hashing password for user '{}' with cost {}", + username, + DEFAULT_COST + ); + let hashed_password = hash(password, DEFAULT_COST).context("Failed to hash password")?; + + // Insert the new user (token and expiry are initially NULL) + log::info!("Creating new user '{}' with ID: {}", username, user_id); + conn.execute( + "INSERT INTO users (id, username, password) VALUES (?1, ?2, ?3)", + params![user_id, username, hashed_password], + ) + .context(format!("Failed to insert user '{}'", username))?; + + Ok(true) // User was added +} + +// Validate a session token and return the associated user ID if valid and not expired +pub fn validate_token(conn: &Connection, token: &str) -> AnyhowResult> { + log::debug!("Validating received token (existence and expiration)..."); + let mut stmt = conn.prepare( + // Select user ID only if token matches AND it hasn't expired + "SELECT id FROM users WHERE token = ?1 AND token_expires_at IS NOT NULL AND token_expires_at > ?2" + ).context("Failed to prepare query for validating token")?; + + let now_ts = Utc::now().to_rfc3339(); // Use ISO 8601 / RFC 3339 format compatible with SQLite DATETIME + + let user_id_option: Option = stmt + .query_row(params![token, now_ts], |row| row.get(0)) + .optional() // Makes it return Option instead of erroring on no rows + .context("Failed to execute query for validating token")?; + + if user_id_option.is_some() { + log::debug!("Token validation successful."); + } else { + // This covers token not found OR token expired + log::debug!("Token validation failed (token not found or expired)."); + } + + Ok(user_id_option) +} + +// Invalidate a user's token (e.g., on logout) by setting it to NULL and clearing expiration +pub fn invalidate_token(conn: &Connection, user_id: &str) -> AnyhowResult<()> { + log::debug!("Invalidating token for user_id {}", user_id); + conn.execute( + "UPDATE users SET token = NULL, token_expires_at = NULL WHERE id = ?1", + params![user_id], + ) + .context(format!( + "Failed to invalidate token for user_id {}", + user_id + ))?; + Ok(()) +} + +// Authenticate a user by username and password, returning user ID and hash if successful +pub fn authenticate_user( + conn: &Connection, + username: &str, + password: &str, +) -> AnyhowResult> { + log::debug!("Attempting to authenticate user: {}", username); + let mut stmt = conn + .prepare("SELECT id, password FROM users WHERE username = ?1") + .context("Failed to prepare query for authenticating user")?; + + let result = stmt + .query_row(params![username], |row| { + Ok(models::UserAuthData { + id: row.get(0)?, + hashed_password: row.get(1)?, + }) + }) + .optional() + .context(format!( + "Failed to execute query to fetch auth data for user '{}'", + username + ))?; + + match result { + Some(user_data) => { + // Verify the provided password against the stored hash + let is_valid = verify(password, &user_data.hashed_password) + .context("Failed to verify password hash")?; + + if is_valid { + log::info!("Authentication successful for user: {}", username); + Ok(Some(user_data)) // Return user ID and hash + } else { + log::warn!( + "Authentication failed for user '{}' (invalid password)", + username + ); + Ok(None) // Invalid password + } + } + None => { + log::warn!( + "Authentication failed for user '{}' (user not found)", + username + ); + Ok(None) // User not found + } + } +} + +// Generate and save a new session token (with expiration) for a user +pub fn generate_and_set_token_for_user(conn: &Connection, user_id: &str) -> AnyhowResult { + let new_token = Uuid::new_v4().to_string(); + // Calculate expiration time + let expires_at = Utc::now() + ChronoDuration::hours(TOKEN_LIFETIME_HOURS); + let expires_at_ts = expires_at.to_rfc3339(); // Store as string + + log::debug!( + "Generating new token for user_id {} expiring at {}", + user_id, + expires_at_ts + ); + + conn.execute( + "UPDATE users SET token = ?1, token_expires_at = ?2 WHERE id = ?3", + params![new_token, expires_at_ts, user_id], + ) + .context(format!("Failed to update token for user_id {}", user_id))?; + + Ok(new_token) +} + +// Fetch a specific form definition by its ID +pub fn get_form_definition(conn: &Connection, form_id: &str) -> AnyhowResult> { + let mut stmt = conn + .prepare("SELECT id, name, fields FROM forms WHERE id = ?1") + .context("Failed to prepare statement for getting form definition")?; + + let form_option = stmt + .query_row(params![form_id], |row| { + let id: String = row.get(0)?; + let name: String = row.get(1)?; + let fields_str: String = row.get(2)?; + + // Ensure fields can be parsed as valid JSON Value + let fields: serde_json::Value = serde_json::from_str(&fields_str).map_err(|e| { + // Log clearly that this is a data integrity issue + log::error!( + "Database integrity error: Failed to parse 'fields' JSON for form_id {}: {}. Content: '{}'", + id, e, fields_str // Log content if not too large/sensitive + ); + rusqlite::Error::FromSqlConversionFailure( + 2, + rusqlite::types::Type::Text, + Box::new(e), + ) + })?; + + // **Basic check**: Ensure fields is an array (common pattern for form definitions) + if !fields.is_array() { + log::error!( + "Database integrity error: 'fields' column for form_id {} is not a JSON array.", + id + ); + return Err(rusqlite::Error::FromSqlConversionFailure( + 2, + rusqlite::types::Type::Text, + "Form fields definition is not a valid JSON array".into(), + )); + } + + Ok(models::Form { + id: Some(id), + name, + fields, + }) + }) + .optional() // Handle case where form_id doesn't exist + .context(format!( + "Failed to execute query for form definition with id {}", + form_id + ))?; + + Ok(form_option) +} + + + +// src/handlers.rs +use crate::auth::Auth; +use crate::models::{Form, LoginCredentials, LoginResponse, Submission}; +use actix_web::{web, Error as ActixWebError, HttpResponse, Responder, Result as ActixResult}; +use anyhow::Context; // Import anyhow::Context for error chaining +use log; +use regex::Regex; // For pattern validation +use rusqlite::{params, Connection}; +use serde_json::{json, Map, Value as JsonValue}; // Alias for clarity +use std::collections::HashMap; +use std::error::Error as StdError; +use std::sync::{Arc, Mutex}; +use uuid::Uuid; + +// --- Helper Function for Database Access --- + +// Gets a database connection from the request data, handling lock errors consistently. +fn get_db_conn( + db: &web::Data>>, +) -> Result, ActixWebError> { + db.lock().map_err(|poisoned| { + log::error!("Database mutex poisoned: {}", poisoned); + actix_web::error::ErrorInternalServerError("Internal database error (mutex lock)") + }) +} + +// --- Helper Function for Validation --- + +/// Validates submission data against the form field definitions with enhanced checks. +/// +/// Expected field definition properties: +/// - `name`: string (required) +/// - `type`: string (e.g., "string", "number", "boolean", "email", "url", "object", "array") (required) +/// - `required`: boolean (optional, default: false) +/// - `maxLength`: number (for "string" type) +/// - `minLength`: number (for "string" type) +/// - `min`: number (for "number" type) +/// - `max`: number (for "number" type) +/// - `pattern`: string (regex for "string", "email", "url" types) +/// +/// Returns `Ok(())` if valid, or `Err(JsonValue)` containing validation errors. +fn validate_submission_against_definition( + submission_data: &JsonValue, + form_definition_fields: &JsonValue, +) -> Result<(), JsonValue> { + let mut errors: HashMap = HashMap::new(); + + // Ensure 'fields' in the definition is a JSON array + let field_definitions = match form_definition_fields.as_array() { + Some(defs) => defs, + None => { + log::error!( + "Form definition 'fields' is not a JSON array. Def: {:?}", + form_definition_fields + ); + errors.insert( + "_internal".to_string(), + "Invalid form definition format (not an array)".to_string(), + ); + return Err(json!({ "validation_errors": errors })); + } + }; + + // Ensure the submission data is a JSON object + let data_map = match submission_data.as_object() { + Some(map) => map, + None => { + errors.insert( + "_submission".to_string(), + "Submission data must be a JSON object".to_string(), + ); + return Err(json!({ "validation_errors": errors })); + } + }; + + // Build a map of valid field names to their definitions from the definition for quick lookup + let defined_field_names: HashMap> = field_definitions + .iter() + .filter_map(|val| val.as_object()) + .filter_map(|def| { + def.get("name") + .and_then(JsonValue::as_str) + .map(|name| (name.to_string(), def)) + }) + .collect(); + + // 1. Check for submitted fields that are NOT in the definition + for submitted_key in data_map.keys() { + if !defined_field_names.contains_key(submitted_key) { + errors.insert( + submitted_key.clone(), + "Unexpected field submitted".to_string(), + ); + } + } + // Exit early if unexpected fields were found + if !errors.is_empty() { + log::warn!("Submission validation failed: Unexpected fields submitted."); + return Err(json!({ "validation_errors": errors })); + } + + // 2. Iterate through each field definition and validate corresponding submitted data + for (field_name, field_def) in &defined_field_names { + // Extract properties using helper functions for clarity + let field_type = field_def + .get("type") + .and_then(JsonValue::as_str) + .unwrap_or("string"); // Default to "string" if type is missing or not a string + let is_required = field_def + .get("required") + .and_then(JsonValue::as_bool) + .unwrap_or(false); // Default to false if required is missing or not a boolean + let min_length = field_def.get("minLength").and_then(JsonValue::as_u64); + let max_length = field_def.get("maxLength").and_then(JsonValue::as_u64); + let min_value = field_def.get("min").and_then(JsonValue::as_f64); // Use f64 for flexibility + let max_value = field_def.get("max").and_then(JsonValue::as_f64); + let pattern = field_def.get("pattern").and_then(JsonValue::as_str); + + match data_map.get(field_name) { + Some(submitted_value) if !submitted_value.is_null() => { + // Field is present and not null, perform type and constraint checks + let mut type_error = None; + let mut constraint_errors = vec![]; + + match field_type { + "string" | "email" | "url" => { + if let Some(s) = submitted_value.as_str() { + if let Some(min) = min_length { + if (s.chars().count() as u64) < min { + // Use chars().count() for UTF-8 correctness + constraint_errors + .push(format!("Must be at least {} characters long", min)); + } + } + if let Some(max) = max_length { + if (s.chars().count() as u64) > max { + constraint_errors.push(format!( + "Must be no more than {} characters long", + max + )); + } + } + if let Some(pat) = pattern { + // Consider caching compiled Regex if performance is critical + // and patterns are reused frequently across requests. + match Regex::new(pat) { + Ok(re) => { + if !re.is_match(s) { + constraint_errors.push(format!("Does not match required pattern")); + } + } + Err(e) => log::warn!("Invalid regex pattern '{}' in form definition for field '{}': {}", pat, field_name, e), // Log regex compilation error + } + } + // Specific checks for email/url + if field_type == "email" { + // Basic email regex (adjust for stricter needs or use a validation crate) + // This regex is very basic and allows many technically invalid addresses. + // Consider crates like `validator` for more robust validation. + let email_regex = + Regex::new(r"^[^\s@]+@[^\s@]+\.[^\s@]+$").unwrap(); // Safe unwrap for known valid regex + if !email_regex.is_match(s) { + constraint_errors + .push("Must be a valid email address".to_string()); + } + } + if field_type == "url" { + // Basic URL check (consider `url` crate for robustness) + if url::Url::parse(s).is_err() { + constraint_errors.push("Must be a valid URL".to_string()); + } + } + } else { + type_error = Some(format!("Expected a string for '{}'", field_name)); + } + } + "number" => { + // Use as_f64 for flexibility (handles integers and floats) + if let Some(num) = submitted_value.as_f64() { + if let Some(min) = min_value { + if num < min { + constraint_errors.push(format!("Must be at least {}", min)); + } + } + if let Some(max) = max_value { + if num > max { + constraint_errors.push(format!("Must be no more than {}", max)); + } + } + } else { + type_error = Some(format!("Expected a number for '{}'", field_name)); + } + } + "boolean" => { + if !submitted_value.is_boolean() { + type_error = Some(format!( + "Expected a boolean (true/false) for '{}'", + field_name + )); + } + } + "object" => { + if !submitted_value.is_object() { + type_error = + Some(format!("Expected a JSON object for '{}'", field_name)); + } + // TODO: Could add deeper validation for object structure here if needed based on definition + } + "array" => { + if !submitted_value.is_array() { + type_error = + Some(format!("Expected a JSON array for '{}'", field_name)); + } + // TODO: Could add validation for array elements here if needed based on definition + } + _ => { + // Log unsupported types during development/debugging if necessary + log::trace!( + "Unsupported field type '{}' encountered during validation for field '{}'. Treating as valid.", + field_type, + field_name + ); + // Assume valid if type is not specifically handled or unknown + } + } + + // Record errors found for this field + if let Some(err) = type_error { + errors.insert(field_name.clone(), err); + } else if !constraint_errors.is_empty() { + // Combine multiple constraint errors if necessary + errors.insert(field_name.clone(), constraint_errors.join("; ")); + } + } // End check for present and non-null value + Some(_) => { + // Value is present but explicitly null (e.g., "fieldName": null) + if is_required { + errors.insert( + field_name.clone(), + "This field is required and cannot be null".to_string(), + ); + } + // Otherwise, null is considered a valid (empty) value for non-required fields + } + None => { + // Field is missing entirely from the submission object + if is_required { + errors.insert(field_name.clone(), "This field is required".to_string()); + } + // Missing is valid for non-required fields + } + } // End match data_map.get(field_name) + } // End loop through field definitions + + // Check if any errors were collected + if errors.is_empty() { + Ok(()) // Validation passed + } else { + log::info!( + "Submission validation failed with {} error(s).", // Log only the count for brevity + errors.len() + ); + // Return a JSON object containing the specific validation errors + Err(json!({ "validation_errors": errors })) + } +} + +// Helper function to convert anyhow::Error to actix_web::Error +fn anyhow_to_actix_error(e: anyhow::Error) -> ActixWebError { + actix_web::error::ErrorInternalServerError(e.to_string()) +} + +// --- Public Handlers --- + +// POST /login +pub async fn login( + db: web::Data>>, + creds: web::Json, +) -> ActixResult { + let db_conn = db.clone(); // Clone Arc for use in web::block + let username = creds.username.clone(); + let password = creds.password.clone(); + + // Wrap the blocking database operations in web::block + let auth_result = web::block(move || { + let conn = db_conn + .lock() + .map_err(|_| anyhow::anyhow!("Database mutex poisoned during login lock"))?; + crate::db::authenticate_user(&conn, &username, &password) + }) + .await + .map_err(|e| { + log::error!("web::block error during authentication: {:?}", e); + actix_web::error::ErrorInternalServerError("Authentication process failed (blocking error)") + })? + .map_err(anyhow_to_actix_error)?; + + match auth_result { + Some(user_data) => { + let db_conn_token = db.clone(); // Clone Arc again for token generation + let user_id = user_data.id.clone(); + + // Generate and store a new token within web::block + let token = web::block(move || { + let conn = db_conn_token + .lock() + .map_err(|_| anyhow::anyhow!("Database mutex poisoned during token lock"))?; + crate::db::generate_and_set_token_for_user(&conn, &user_id) + }) + .await + .map_err(|e| { + log::error!("web::block error during token generation: {:?}", e); + actix_web::error::ErrorInternalServerError( + "Failed to complete login (token generation blocking error)", + ) + })? + .map_err(anyhow_to_actix_error)?; + + log::info!("Login successful for user_id: {}", user_data.id); + Ok(HttpResponse::Ok().json(LoginResponse { token })) + } + None => { + log::warn!("Login failed for username: {}", creds.username); + // Return 401 Unauthorized for failed login attempts + Err(actix_web::error::ErrorUnauthorized( + "Invalid username or password", + )) + } + } +} + +// POST /logout +pub async fn logout( + db: web::Data>>, + auth: Auth, // Requires authentication (extracts user_id from token) +) -> ActixResult { + log::info!("User {} requesting logout", auth.user_id); + let db_conn = db.clone(); + let user_id = auth.user_id.clone(); + + // Invalidate the token in the database within web::block + web::block(move || { + let conn = db_conn + .lock() + .map_err(|_| anyhow::anyhow!("Database mutex poisoned during logout lock"))?; + crate::db::invalidate_token(&conn, &user_id) + }) + .await + .map_err(|e| { + let user_id = auth.user_id.clone(); // Clone user_id again after the move + log::error!( + "web::block error during logout for user {}: {:?}", + user_id, + e + ); + actix_web::error::ErrorInternalServerError("Logout failed (blocking error)") + })? + .map_err(anyhow_to_actix_error)?; + + log::info!("User {} logged out successfully", auth.user_id); + Ok(HttpResponse::Ok().json(json!({ "message": "Logged out successfully" }))) +} + +// POST /forms/{form_id}/submissions +pub async fn submit_form( + db: web::Data>>, + path: web::Path, // Extracts form_id from path + submission_payload: web::Json, // Expect arbitrary JSON payload +) -> ActixResult { + let form_id = path.into_inner(); + let submission_data = submission_payload.into_inner(); // Get the JSON data + + // --- Stage 1: Fetch form definition (Read-only, can use shared lock) --- + let form_definition = { + // Acquire lock temporarily for the read operation + let conn = get_db_conn(&db)?; + match crate::db::get_form_definition(&conn, &form_id) { + Ok(Some(form)) => form, + Ok(None) => { + log::warn!("Submission attempt for non-existent form_id: {}", form_id); + return Err(actix_web::error::ErrorNotFound("Form not found")); + } + Err(e) => { + log::error!("Failed to fetch form definition for {}: {:?}", form_id, e); + return Err(actix_web::error::ErrorInternalServerError( + "Could not retrieve form information", + )); + } + } + // Lock is released here when 'conn' goes out of scope + }; + + // --- Stage 2: Validate submission against definition (CPU-bound, no DB lock needed) --- + if let Err(validation_errors) = + validate_submission_against_definition(&submission_data, &form_definition.fields) + { + log::warn!( + "Submission validation failed for form_id {}. Errors: {:?}", // Log actual errors if needed (might be verbose) + form_id, + validation_errors + ); + // Return 400 Bad Request with validation error details + return Ok(HttpResponse::BadRequest().json(validation_errors)); + } + + // --- Stage 3: Serialize validated data and Insert submission (Write operation, use web::block) --- + let submission_json = match serde_json::to_string(&submission_data) { + Ok(json_string) => json_string, + Err(e) => { + log::error!( + "Failed to serialize validated submission data for form {}: {}", + form_id, + e + ); + return Err(actix_web::error::ErrorInternalServerError( + "Failed to process submission data internally", + )); + } + }; + + let db_conn_write = db.clone(); // Clone Arc for the blocking operation + let form_id_clone = form_id.clone(); // Clone for closure + let submission_id = Uuid::new_v4().to_string(); // Generate unique ID for the submission + let submission_id_clone = submission_id.clone(); // Clone for closure + + web::block(move || { + let conn = db_conn_write.lock().map_err(|_| { + anyhow::anyhow!("Database mutex poisoned during submission insert lock") + })?; + conn.execute( + "INSERT INTO submissions (id, form_id, data) VALUES (?1, ?2, ?3)", + params![submission_id_clone, form_id_clone, submission_json], + ) + .context(format!( + "Failed to insert submission for form {}", + form_id_clone + )) + .map_err(anyhow::Error::from) + }) + .await + .map_err(|e| { + log::error!( + "web::block error during submission insertion for form {}: {:?}", + form_id, + e + ); + actix_web::error::ErrorInternalServerError("Failed to save submission (blocking error)") + })? + .map_err(anyhow_to_actix_error)?; + + log::info!( + "Successfully inserted submission {} for form_id {}", + submission_id, + form_id + ); + // Return 200 OK with the new submission ID + Ok(HttpResponse::Ok().json(json!({ "submission_id": submission_id }))) +} + +// --- Protected Handlers (Require Auth) --- + +// POST /forms +pub async fn create_form( + db: web::Data>>, + auth: Auth, // Authentication check via Auth extractor + form_payload: web::Json
, +) -> ActixResult { + log::info!( + "User {} attempting to create form: {}", + auth.user_id, + form_payload.name + ); + + let mut form = form_payload.into_inner(); + // Generate a new UUID for the form if not provided (or overwrite if provided) + let form_id = form.id.unwrap_or_else(|| Uuid::new_v4().to_string()); + form.id = Some(form_id.clone()); // Ensure the form object has the ID + + // Basic structural validation: Ensure 'fields' is a JSON array before serialization/saving + if !form.fields.is_array() { + log::error!( + "User {} attempted to create form '{}' ('{}') where 'fields' is not a JSON array.", + auth.user_id, + form.name, + form_id + ); + return Err(actix_web::error::ErrorBadRequest( + "Form 'fields' must be a valid JSON array.", + )); + } + // TODO: Add deeper validation of the 'fields' structure itself if needed + // e.g., check if each element in 'fields' is an object with 'name' and 'type'. + + // Serialize the fields part to JSON string for DB storage + let fields_json = match serde_json::to_string(&form.fields) { + Ok(json_str) => json_str, + Err(e) => { + log::error!( + "Failed to serialize form fields for form '{}' ('{}') by user {}: {}", + form.name, + form_id, + auth.user_id, + e + ); + return Err(actix_web::error::ErrorInternalServerError( + "Failed to process form fields internally", + )); + } + }; + + // Clone data needed for the blocking database operation + let db_conn = db.clone(); + // let form_id = form_id; // Already have it from above + let form_name = form.name.clone(); + let user_id = auth.user_id.clone(); // For logging inside block if needed + + // Insert the form using web::block for the blocking DB write + web::block(move || { + let conn = db_conn + .lock() + .map_err(|_| anyhow::anyhow!("Database mutex poisoned during form creation lock"))?; + conn.execute( + // Consider adding user_id to the forms table if forms are user-specific + "INSERT INTO forms (id, name, fields) VALUES (?1, ?2, ?3)", + params![form_id, form_name, fields_json], + ) + .context("Failed to insert new form into database") + .map_err(anyhow::Error::from) + }) + .await + .map_err(|e| { + log::error!( + "web::block error during form creation by user {}: {:?}", + auth.user_id, + e + ); + actix_web::error::ErrorInternalServerError("Failed to create form (blocking error)") + })? + .map_err(anyhow_to_actix_error)?; + + log::info!( + "Successfully created form '{}' with id {} by user {}", + form.name, + form.id.as_ref().unwrap(), // Safe unwrap as we set it + auth.user_id + ); + // Return 200 OK with the newly created form object (including its ID) + Ok(HttpResponse::Ok().json(form)) +} + +// GET /forms +pub async fn get_forms( + db: web::Data>>, + auth: Auth, // Requires authentication +) -> ActixResult { + log::info!("User {} requesting list of forms", auth.user_id); + let db_conn = db.clone(); + let user_id = auth.user_id.clone(); // Clone for logging context if needed inside block + + // Wrap DB query in web::block as it might be slow with many forms or complex parsing + let forms_result = web::block(move || { + let conn = db_conn + .lock() + .map_err(|_| anyhow::anyhow!("Database mutex poisoned during get_forms lock"))?; + + let mut stmt = conn + .prepare("SELECT id, name, fields FROM forms") + .context("Failed to prepare statement for getting forms")?; + + let forms_iter = stmt + .query_map([], |row| { + let id: String = row.get(0)?; + let name: String = row.get(1)?; + let fields_str: String = row.get(2)?; + + // Parse the 'fields' JSON string. If it fails, log the error and skip the row. + let fields: serde_json::Value = match serde_json::from_str(&fields_str) { + Ok(json_value) => json_value, + Err(e) => { + // Log the data integrity issue clearly + log::error!( + "DB Parse Error: Failed to parse 'fields' JSON for form id {}: {}. Skipping this form.", + id, e + ); + // Return a special error that `filter_map` below can catch, + // without failing the entire query_map. + // Using a specific rusqlite error type here is okay. + return Err(rusqlite::Error::FromSqlConversionFailure( + 2, // Column index + rusqlite::types::Type::Text, + Box::new(e) // Box the original error + )); + } + }; + + Ok(Form { id: Some(id), name, fields }) + }) + .context("Failed to execute query map for getting forms")?; + + // Collect results, filtering out rows that failed parsing WITHIN the block + let forms: Vec = forms_iter + .filter_map(|result| match result { + Ok(form) => Some(form), + Err(e) => { + // Error was already logged inside the query_map closure. + // We just filter out the failed row here. + log::warn!("Skipping a form row due to a processing error: {}", e); + None // Skip this row + } + }) + .collect(); + + Ok::<_, anyhow::Error>(forms) // Ensure block returns Result compatible with flattening + }) + .await + .map_err(|e| { + // Handle web::block error + log::error!("web::block error during get_forms for user {}: {:?}", user_id, e); + actix_web::error::ErrorInternalServerError("Failed to retrieve forms (blocking error)") + })? + .map_err(anyhow_to_actix_error)?; // Flatten Result, anyhow::Error>, BlockingError> + + log::debug!( + "Returning {} forms for user {}", + forms_result.len(), + auth.user_id + ); + Ok(HttpResponse::Ok().json(forms_result)) +} + +// GET /forms/{form_id}/submissions +pub async fn get_submissions( + db: web::Data>>, + auth: Auth, // Requires authentication + path: web::Path, // Extracts form_id from the path +) -> ActixResult { + let form_id = path.into_inner(); + log::info!( + "User {} requesting submissions for form_id: {}", + auth.user_id, + form_id + ); + + let db_conn = db.clone(); + let form_id_clone = form_id.clone(); + let user_id = auth.user_id.clone(); // Clone for logging context + + // Wrap DB queries (existence check + fetching submissions) in web::block + let submissions_result = web::block(move || { + let conn = db_conn + .lock() + .map_err(|_| anyhow::anyhow!("Database mutex poisoned during get_submissions lock"))?; + + // 1. Check if the form exists first + let form_exists: bool = match conn.query_row( + "SELECT EXISTS(SELECT 1 FROM forms WHERE id = ?1 LIMIT 1)", // Added LIMIT 1 for potential optimization + params![form_id_clone], + |row| row.get::<_, i32>(0), // sqlite returns 0 or 1 for EXISTS + ) { + Ok(count) => count == 1, + Err(rusqlite::Error::QueryReturnedNoRows) => false, // Should not happen with EXISTS, but handle defensively + Err(e) => return Err(anyhow::Error::from(e) // Propagate other DB errors + .context(format!("Failed check existence of form {}", form_id_clone))), + }; + + if !form_exists { + // Use Ok(None) to signal "form not found" to the calling async context + return Ok(None); + } + + // 2. If form exists, fetch its submissions + let mut stmt = conn.prepare( + "SELECT id, form_id, data, created_at FROM submissions WHERE form_id = ?1 ORDER BY created_at DESC", // Include created_at if needed + ) + .context(format!("Failed to prepare statement for getting submissions for form {}", form_id_clone))?; + + let submissions_iter = stmt + .query_map(params![form_id_clone], |row| { + let id: String = row.get(0)?; + let form_id_db: String = row.get(1)?; + let data_str: String = row.get(2)?; + // let created_at: String = row.get(3)?; // Example: If you fetch created_at + + // Parse the 'data' JSON string, handling potential errors + let data: serde_json::Value = match serde_json::from_str(&data_str) { + Ok(json_value) => json_value, + Err(e) => { + log::error!( + "DB Parse Error: Failed to parse 'data' JSON for submission_id {}: {}. Skipping.", + id, e + ); + // Return specific error for filter_map + return Err(rusqlite::Error::FromSqlConversionFailure( + 2, rusqlite::types::Type::Text, Box::new(e) + )); + } + }; + + Ok(Submission { id, form_id: form_id_db, data }) // Add created_at if fetched + }) + .context(format!("Failed to execute query map for getting submissions for form {}", form_id_clone))?; + + // Collect valid submissions, filtering out rows that failed parsing + let submissions: Vec = submissions_iter + .filter_map(|result| match result { + Ok(submission) => Some(submission), + Err(e) => { + log::warn!("Skipping a submission row due to processing error: {}", e); + None // Skip this row + } + }) + .collect(); + + Ok(Some(submissions)) // Indicate success with the (potentially empty) list of submissions + + }) + .await + .map_err(|e| { // Handle web::block error (cancellation, panic) + log::error!("web::block error during get_submissions for form {} by user {}: {:?}", form_id, user_id, e); + actix_web::error::ErrorInternalServerError("Failed to retrieve submissions (blocking error)") + })? + .map_err(anyhow_to_actix_error)?; // Flatten Result>, anyhow::Error>, BlockingError> + + // Process the result obtained from the web::block + match submissions_result { + Some(submissions) => { + // Form exists, return the found submissions (might be an empty list) + log::debug!( + "Returning {} submissions for form {} requested by user {}", + submissions.len(), + form_id, + auth.user_id + ); + Ok(HttpResponse::Ok().json(submissions)) + } + None => { + // Form was not found (signaled by Ok(None) from the block) + log::warn!( + "Attempt by user {} to get submissions for non-existent form_id: {}", + auth.user_id, + form_id + ); + Err(actix_web::error::ErrorNotFound("Form not found")) + } + } +} + + + +// src/main.rs +use actix_cors::Cors; +use actix_files as fs; +use actix_web::{http::header, middleware::Logger, web, App, HttpServer}; // Added Logger explicitly +use dotenv::dotenv; +use log; +use std::env; +use std::io::Result as IoResult; // Alias for clarity +use std::process; +use std::sync::{Arc, Mutex}; + +// Import modules +mod auth; +mod db; +mod handlers; +mod models; + +#[actix_web::main] +async fn main() -> IoResult<()> { + dotenv().ok(); // Load .env file + + // Initialize logger (using RUST_LOG env var) + env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init(); + + // --- Configuration (Environment Variables) --- + // CRITICAL: Database URL is required + let database_url = env::var("DATABASE_URL").unwrap_or_else(|_| { + log::warn!("DATABASE_URL environment variable not set. Defaulting to 'form_data.db'."); + "form_data.db".to_string() + }); + // CRITICAL: Bind address is required + let bind_address = env::var("BIND_ADDRESS").unwrap_or_else(|_| { + log::warn!("BIND_ADDRESS environment variable not set. Defaulting to '127.0.0.1:8080'."); + "127.0.0.1:8080".to_string() + }); + // CRITICAL: Initial admin credentials (checked in db::init_db) + // let initial_admin_username = env::var("INITIAL_ADMIN_USERNAME").expect("Missing INITIAL_ADMIN_USERNAME"); + // let initial_admin_password = env::var("INITIAL_ADMIN_PASSWORD").expect("Missing INITIAL_ADMIN_PASSWORD"); + // OPTIONAL: Allowed origin for CORS + let allowed_origin = env::var("ALLOWED_ORIGIN").ok(); // Use ok() to make it optional + + log::info!(" --- Formies Backend Configuration ---"); + log::info!("Required Environment Variables:"); + log::info!(" - DATABASE_URL (Current: {})", database_url); + log::info!(" - BIND_ADDRESS (Current: {})", bind_address); + log::info!(" - INITIAL_ADMIN_USERNAME (Set during startup, MUST be present)"); + log::info!(" - INITIAL_ADMIN_PASSWORD (Set during startup, MUST be present)"); + log::info!("Optional Environment Variables:"); + if let Some(ref origin) = allowed_origin { + log::info!(" - ALLOWED_ORIGIN (Set: {})", origin); + } else { + log::warn!(" - ALLOWED_ORIGIN (Not Set): CORS will be restrictive, potentially blocking browser access. Set to your frontend URL (e.g., http://localhost:5173 or https://yourdomain.com)."); + } + log::info!(" - RUST_LOG (e.g., 'info,formies_be=debug')"); + log::info!(" --- End Configuration ---"); + + // Initialize database connection + let db_connection = match db::init_db(&database_url) { + Ok(conn) => conn, + Err(e) => { + // Specific check for missing admin credentials error + if e.to_string().contains("INITIAL_ADMIN_USERNAME") + || e.to_string().contains("INITIAL_ADMIN_PASSWORD") + { + log::error!("FATAL: {}", e); + log::error!("Please set the INITIAL_ADMIN_USERNAME and INITIAL_ADMIN_PASSWORD environment variables."); + } else { + log::error!( + "FATAL: Failed to initialize database at {}: {:?}", + database_url, + e + ); + } + process::exit(1); // Exit if DB initialization fails + } + }; + + // Wrap connection in Arc> for thread-safe sharing + let db_data = web::Data::new(Arc::new(Mutex::new(db_connection))); + + log::info!("Starting server at http://{}", bind_address); + + HttpServer::new(move || { + // Clone shared state for the closure + let db_data_clone = db_data.clone(); + let allowed_origin_clone = allowed_origin.clone(); + + // Configure CORS + let cors = match allowed_origin_clone { + Some(origin) => { + log::info!("Configuring CORS for specific origin: {}", origin); + Cors::default() + .allowed_origin(&origin) // Allow only the specified origin + .allowed_methods(vec!["GET", "POST", "PUT", "DELETE", "OPTIONS"]) + .allowed_headers(vec![ + header::AUTHORIZATION, + header::ACCEPT, + header::CONTENT_TYPE, + header::ORIGIN, // Add Origin header if needed + header::ACCESS_CONTROL_REQUEST_METHOD, + header::ACCESS_CONTROL_REQUEST_HEADERS, + ]) + .supports_credentials() + .max_age(3600) + } + None => { + // Default restrictive CORS: No origin allowed explicitly. + // This will likely block browser requests unless the browser and server are on the same origin. + log::warn!("CORS is configured restrictively: No ALLOWED_ORIGIN set."); + Cors::default() // No allowed_origin set + .allowed_methods(vec!["GET", "POST", "PUT", "DELETE", "OPTIONS"]) + .allowed_headers(vec![ + header::AUTHORIZATION, + header::ACCEPT, + header::CONTENT_TYPE, + header::ORIGIN, + header::ACCESS_CONTROL_REQUEST_METHOD, + header::ACCESS_CONTROL_REQUEST_HEADERS, + ]) + .supports_credentials() + .max_age(3600) + // DO NOT use allow_any_origin() unless you fully understand the security implications. + } + }; + + App::new() + .wrap(cors) // Apply CORS middleware + .wrap(Logger::default()) // Add request logging (default format) + .app_data(db_data_clone) // Share database connection pool + // --- API Routes --- + .service( + web::scope("/api") // Group API routes under /api + // --- Public Routes --- + .route("/login", web::post().to(handlers::login)) + .route( + "/forms/{form_id}/submissions", + web::post().to(handlers::submit_form), + ) + // --- Protected Routes (using Auth extractor) --- + .route("/logout", web::post().to(handlers::logout)) // Added logout + .route("/forms", web::post().to(handlers::create_form)) + .route("/forms", web::get().to(handlers::get_forms)) + .route( + "/forms/{form_id}/submissions", + web::get().to(handlers::get_submissions), + ), + ) + // --- Static Files (Serve Frontend - Optional) --- + // Assumes frontend build output is in ../frontend/dist + // Register this LAST to avoid conflicts with API routes + .service( + fs::Files::new("/", "../frontend/dist/") + .index_file("index.html") + .use_last_modified(true) + // Optional: Add a fallback to index.html for SPA routing + .default_handler( + fs::NamedFile::open("../frontend/dist/index.html").unwrap_or_else(|_| { + log::error!("Fallback file not found: ../frontend/dist/index.html"); + process::exit(1); // Exit if fallback file is missing + }), // Handle error explicitly + ), + ) + }) + .bind(&bind_address)? + .run() + .await +} + + + +// src/models.rs +use serde::{Deserialize, Serialize}; +// Consider adding chrono for DateTime types if needed in responses +// use chrono::{DateTime, Utc}; + +// Represents the structure for defining a form +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct Form { + #[serde(skip_serializing_if = "Option::is_none")] + pub id: Option, + pub name: String, + /// Stores the structure defining the form fields. + /// Expected to be a JSON array of field definition objects. + /// Example field definition object: + /// ```json + /// { + /// "name": "email", // String, required: Unique identifier for the field + /// "type": "email", // String, required: "string", "number", "boolean", "email", "url", "object", "array" + /// "label": "Email Address", // String, optional: User-friendly label + /// "required": true, // Boolean, optional (default: false): If the field must have a value + /// "placeholder": "you@example.com", // String, optional: Placeholder text + /// "minLength": 5, // Number, optional: Minimum length for strings + /// "maxLength": 100, // Number, optional: Maximum length for strings + /// "min": 0, // Number, optional: Minimum value for numbers + /// "max": 100, // Number, optional: Maximum value for numbers + /// "pattern": "^\\S+@\\S+\\.\\S+$" // String, optional: Regex pattern for strings (e.g., email, url types might use this implicitly or explicitly) + /// // Add other properties like "options" for select/radio, etc. + /// } + /// ``` + pub fields: serde_json::Value, + // Optional: Add created_at if needed in API responses + // pub created_at: Option>, +} + +// Represents a single submission for a specific form +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct Submission { + pub id: String, + pub form_id: String, + /// Stores the data submitted by the user. + /// Expected to be a JSON object where keys are field names (`name`) from the form definition's `fields` array. + /// Example: `{ "email": "user@example.com", "age": 30 }` + pub data: serde_json::Value, + // Optional: Add created_at if needed in API responses + // pub created_at: Option>, +} + +// Used for the /login endpoint request body +#[derive(Debug, Serialize, Deserialize)] +pub struct LoginCredentials { + pub username: String, + pub password: String, +} + +// Used for the /login endpoint response body +#[derive(Debug, Serialize, Deserialize)] +pub struct LoginResponse { + pub token: String, // The session token (UUID) +} + +// Used internally to represent a user fetched from the DB for authentication check +// Not serialized, only used within db.rs and handlers.rs +#[derive(Debug)] +pub struct UserAuthData { + pub id: String, + pub hashed_password: String, + // Note: Token and expiry are handled separately and not needed in this specific struct +} + +// --- Custom Application Error (Optional but Recommended for Consistency) --- +// Although not fully integrated in this pass to minimize changes, +// this shows the structure for future improvement. + +// use actix_web::{ResponseError, http::StatusCode}; +// use std::fmt; + +// #[derive(Debug)] +// pub enum AppError { +// DatabaseError(anyhow::Error), +// ConfigError(String), +// ValidationError(serde_json::Value), // Store the validation errors JSON +// NotFound(String), +// Unauthorized(String), +// InternalError(String), +// BlockingError(String), +// } + +// impl fmt::Display for AppError { +// fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { +// match self { +// AppError::DatabaseError(e) => write!(f, "Database error: {}", e), +// AppError::ConfigError(s) => write!(f, "Configuration error: {}", s), +// AppError::ValidationError(_) => write!(f, "Validation failed"), +// AppError::NotFound(s) => write!(f, "Not found: {}", s), +// AppError::Unauthorized(s) => write!(f, "Unauthorized: {}", s), +// AppError::InternalError(s) => write!(f, "Internal server error: {}", s), +// AppError::BlockingError(s) => write!(f, "Blocking operation error: {}", s), +// } +// } +// } + +// impl ResponseError for AppError { +// fn status_code(&self) -> StatusCode { +// match self { +// AppError::DatabaseError(_) => StatusCode::INTERNAL_SERVER_ERROR, +// AppError::ConfigError(_) => StatusCode::INTERNAL_SERVER_ERROR, +// AppError::ValidationError(_) => StatusCode::BAD_REQUEST, +// AppError::NotFound(_) => StatusCode::NOT_FOUND, +// AppError::Unauthorized(_) => StatusCode::UNAUTHORIZED, +// AppError::InternalError(_) => StatusCode::INTERNAL_SERVER_ERROR, +// AppError::BlockingError(_) => StatusCode::INTERNAL_SERVER_ERROR, +// } +// } + +// fn error_response(&self) -> HttpResponse { +// let status = self.status_code(); +// let error_json = match self { +// AppError::ValidationError(errors) => errors.clone(), +// // Provide a generic error structure for others +// _ => json!({ "error": status.canonical_reason().unwrap_or("Unknown Error"), "message": self.to_string() }), +// }; + +// HttpResponse::build(status).json(error_json) +// } +// } + +// // Implement From traits to convert other errors into AppError easily +// impl From for AppError { +// fn from(err: anyhow::Error) -> Self { +// // Basic conversion, could add more context analysis here +// AppError::DatabaseError(err) +// } +// } +// impl From for AppError { +// fn from(err: actix_web::error::BlockingError) -> Self { +// AppError::BlockingError(err.to_string()) +// } +//} +// // Add From, From, etc. as needed + + + diff --git a/src/auth.rs b/src/auth.rs new file mode 100644 index 0000000..34645ca --- /dev/null +++ b/src/auth.rs @@ -0,0 +1,99 @@ +// src/auth.rs +use actix_web::error::{ErrorInternalServerError, ErrorUnauthorized}; // Specific error types +use actix_web::{ + dev::Payload, http::header::AUTHORIZATION, web, Error as ActixWebError, FromRequest, + HttpRequest, Result as ActixResult, +}; +use chrono::{Duration, Utc}; // Import chrono for time checks +use futures::future::{ready, Ready}; +use log; // Use the log crate +use rusqlite::Connection; +use std::sync::{Arc, Mutex}; + +// Represents an authenticated user via token +pub struct Auth { + pub user_id: String, +} + +impl FromRequest for Auth { + // Use actix_web::Error for consistency in error handling within Actix + type Error = ActixWebError; + // Use Ready from futures 0.3 + type Future = Ready>; + + fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future { + // Extract database connection pool from application data + // Replace .expect() with proper error handling + let db_data_result = req.app_data::>>>(); + + let db_data = match db_data_result { + Some(data) => data, + None => { + log::error!("Database connection missing in application data configuration."); + return ready(Err(ErrorInternalServerError( + "Internal server error (app configuration)", + ))); + } + }; + + // Extract Authorization header + let auth_header = req.headers().get(AUTHORIZATION); + + if let Some(auth_header_value) = auth_header { + // Convert header value to string + if let Ok(auth_str) = auth_header_value.to_str() { + // Check if it starts with "Bearer " + if auth_str.starts_with("Bearer ") { + // Extract the token part + let token = &auth_str[7..]; + + // Lock the mutex to get access to the connection + // Handle potential mutex poisoning explicitly + let conn_guard = match db_data.lock() { + Ok(guard) => guard, + Err(poisoned) => { + log::error!("Database mutex poisoned: {}", poisoned); + // Return internal server error if mutex is poisoned + return ready(Err(ErrorInternalServerError( + "Internal server error (database lock)", + ))); + } + }; + + // Validate the token against the database (now includes expiration check) + match super::db::validate_token(&conn_guard, token) { + // Token is valid and not expired, return Ok with Auth struct + Ok(Some(user_id)) => { + log::debug!("Token validated successfully for user_id: {}", user_id); + ready(Ok(Auth { user_id })) + } + // Token is invalid, not found, or expired + Ok(None) => { + log::warn!("Invalid or expired token received"); // Avoid logging token + ready(Err(ErrorUnauthorized("Invalid or expired token"))) + } + // Database error during token validation + Err(e) => { + log::error!("Database error during token validation: {:?}", e); + // Return Unauthorized to avoid leaking internal error details + // Consider mapping specific DB errors if needed, but Unauthorized is generally safe + ready(Err(ErrorUnauthorized("Token validation failed"))) + } + } + } else { + // Header present but not "Bearer " format + log::warn!("Invalid Authorization header format (not Bearer)"); + ready(Err(ErrorUnauthorized("Invalid token format"))) + } + } else { + // Header value contains invalid characters + log::warn!("Authorization header contains invalid characters"); + ready(Err(ErrorUnauthorized("Invalid token value"))) + } + } else { + // Authorization header is missing + log::warn!("Missing Authorization header"); + ready(Err(ErrorUnauthorized("Missing authorization token"))) + } + } +} diff --git a/src/db.rs b/src/db.rs new file mode 100644 index 0000000..a9e9f30 --- /dev/null +++ b/src/db.rs @@ -0,0 +1,302 @@ +// src/db.rs +use anyhow::{anyhow, Context, Result as AnyhowResult}; +use bcrypt::{hash, verify, DEFAULT_COST}; +use chrono::{Duration as ChronoDuration, Utc}; // Use Utc for timestamps +use log; // Use the log crate +use rusqlite::{ + params, types::Value as RusqliteValue, Connection, OptionalExtension, Result as RusqliteResult, +}; +use std::env; +use uuid::Uuid; + +use crate::models; + +// Configurable token lifetime (e.g., from environment variable or default) +const TOKEN_LIFETIME_HOURS: i64 = 24; // Default to 24 hours + +// Initialize the database connection and create tables if they don't exist +pub fn init_db(database_url: &str) -> AnyhowResult { + log::info!("Attempting to open or create database at: {}", database_url); + let conn = Connection::open(database_url) + .context(format!("Failed to open the database at {}", database_url))?; + + log::debug!("Creating 'users' table if not exists..."); + conn.execute( + "CREATE TABLE IF NOT EXISTS users ( + id TEXT PRIMARY KEY, + username TEXT NOT NULL UNIQUE, + password TEXT NOT NULL, -- Stores bcrypt hashed password + token TEXT UNIQUE, -- Stores the current session token (UUID) + token_expires_at DATETIME -- Timestamp when the token expires + )", + [], + ) + .context("Failed to create 'users' table")?; + + log::debug!("Creating 'forms' table if not exists..."); + // Storing complex form definitions as JSON blobs in TEXT columns is pragmatic + // but sacrifices DB-level type safety and query capabilities. Ensure robust + // application-level validation and consider backup strategies carefully. + conn.execute( + "CREATE TABLE IF NOT EXISTS forms ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL, + fields TEXT NOT NULL, -- Stores JSON definition of form fields + created_at DATETIME DEFAULT CURRENT_TIMESTAMP + )", + [], + ) + .context("Failed to create 'forms' table")?; + + log::debug!("Creating 'submissions' table if not exists..."); + // Storing submission data as JSON blobs has similar tradeoffs as form fields. + conn.execute( + "CREATE TABLE IF NOT EXISTS submissions ( + id TEXT PRIMARY KEY, + form_id TEXT NOT NULL, + data TEXT NOT NULL, -- Stores JSON submission data + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (form_id) REFERENCES forms (id) ON DELETE CASCADE + )", + [], + ) + .context("Failed to create 'submissions' table")?; + + // Setup the initial admin user if it doesn't exist, using environment variables + setup_initial_admin(&conn).context("Failed to setup initial admin user")?; + + log::info!("Database initialization complete."); + Ok(conn) +} + +// Sets up the initial admin user from *required* environment variables if it doesn't exist +fn setup_initial_admin(conn: &Connection) -> AnyhowResult<()> { + // CRITICAL SECURITY CHANGE: Remove default credentials. Require env vars. + let initial_admin_username = env::var("INITIAL_ADMIN_USERNAME") + .map_err(|_| anyhow!("FATAL: INITIAL_ADMIN_USERNAME environment variable is not set."))?; + let initial_admin_password = env::var("INITIAL_ADMIN_PASSWORD") + .map_err(|_| anyhow!("FATAL: INITIAL_ADMIN_PASSWORD environment variable is not set."))?; + + if initial_admin_username.is_empty() || initial_admin_password.is_empty() { + return Err(anyhow!( + "FATAL: INITIAL_ADMIN_USERNAME and INITIAL_ADMIN_PASSWORD must not be empty." + )); + } + + // Check password complexity? (Optional enhancement) + + add_user_if_not_exists(conn, &initial_admin_username, &initial_admin_password) + .context("Failed during initial admin user setup")?; + Ok(()) +} + +// Adds a user with a hashed password if the username doesn't exist +pub fn add_user_if_not_exists( + conn: &Connection, + username: &str, + password: &str, +) -> AnyhowResult { + // Check if user already exists + let user_exists: bool = conn + .query_row( + "SELECT EXISTS(SELECT 1 FROM users WHERE username = ?1)", + params![username], + |row| row.get::<_, i32>(0), + ) + .context(format!("Failed to check existence of user '{}'", username))? + == 1; + + if user_exists { + log::debug!("User '{}' already exists, skipping creation.", username); + return Ok(false); // User already exists, nothing added + } + + // Generate a UUID for the new user + let user_id = Uuid::new_v4().to_string(); + + // Hash the password using bcrypt + // Ensure the cost factor is appropriate for your security needs and hardware. + // Higher cost means slower hashing and verification, but better resistance to brute-force. + log::debug!( + "Hashing password for user '{}' with cost {}", + username, + DEFAULT_COST + ); + let hashed_password = hash(password, DEFAULT_COST).context("Failed to hash password")?; + + // Insert the new user (token and expiry are initially NULL) + log::info!("Creating new user '{}' with ID: {}", username, user_id); + conn.execute( + "INSERT INTO users (id, username, password) VALUES (?1, ?2, ?3)", + params![user_id, username, hashed_password], + ) + .context(format!("Failed to insert user '{}'", username))?; + + Ok(true) // User was added +} + +// Validate a session token and return the associated user ID if valid and not expired +pub fn validate_token(conn: &Connection, token: &str) -> AnyhowResult> { + log::debug!("Validating received token (existence and expiration)..."); + let mut stmt = conn.prepare( + // Select user ID only if token matches AND it hasn't expired + "SELECT id FROM users WHERE token = ?1 AND token_expires_at IS NOT NULL AND token_expires_at > ?2" + ).context("Failed to prepare query for validating token")?; + + let now_ts = Utc::now().to_rfc3339(); // Use ISO 8601 / RFC 3339 format compatible with SQLite DATETIME + + let user_id_option: Option = stmt + .query_row(params![token, now_ts], |row| row.get(0)) + .optional() // Makes it return Option instead of erroring on no rows + .context("Failed to execute query for validating token")?; + + if user_id_option.is_some() { + log::debug!("Token validation successful."); + } else { + // This covers token not found OR token expired + log::debug!("Token validation failed (token not found or expired)."); + } + + Ok(user_id_option) +} + +// Invalidate a user's token (e.g., on logout) by setting it to NULL and clearing expiration +pub fn invalidate_token(conn: &Connection, user_id: &str) -> AnyhowResult<()> { + log::debug!("Invalidating token for user_id {}", user_id); + conn.execute( + "UPDATE users SET token = NULL, token_expires_at = NULL WHERE id = ?1", + params![user_id], + ) + .context(format!( + "Failed to invalidate token for user_id {}", + user_id + ))?; + Ok(()) +} + +// Authenticate a user by username and password, returning user ID and hash if successful +pub fn authenticate_user( + conn: &Connection, + username: &str, + password: &str, +) -> AnyhowResult> { + log::debug!("Attempting to authenticate user: {}", username); + let mut stmt = conn + .prepare("SELECT id, password FROM users WHERE username = ?1") + .context("Failed to prepare query for authenticating user")?; + + let result = stmt + .query_row(params![username], |row| { + Ok(models::UserAuthData { + id: row.get(0)?, + hashed_password: row.get(1)?, + }) + }) + .optional() + .context(format!( + "Failed to execute query to fetch auth data for user '{}'", + username + ))?; + + match result { + Some(user_data) => { + // Verify the provided password against the stored hash + let is_valid = verify(password, &user_data.hashed_password) + .context("Failed to verify password hash")?; + + if is_valid { + log::info!("Authentication successful for user: {}", username); + Ok(Some(user_data)) // Return user ID and hash + } else { + log::warn!( + "Authentication failed for user '{}' (invalid password)", + username + ); + Ok(None) // Invalid password + } + } + None => { + log::warn!( + "Authentication failed for user '{}' (user not found)", + username + ); + Ok(None) // User not found + } + } +} + +// Generate and save a new session token (with expiration) for a user +pub fn generate_and_set_token_for_user(conn: &Connection, user_id: &str) -> AnyhowResult { + let new_token = Uuid::new_v4().to_string(); + // Calculate expiration time + let expires_at = Utc::now() + ChronoDuration::hours(TOKEN_LIFETIME_HOURS); + let expires_at_ts = expires_at.to_rfc3339(); // Store as string + + log::debug!( + "Generating new token for user_id {} expiring at {}", + user_id, + expires_at_ts + ); + + conn.execute( + "UPDATE users SET token = ?1, token_expires_at = ?2 WHERE id = ?3", + params![new_token, expires_at_ts, user_id], + ) + .context(format!("Failed to update token for user_id {}", user_id))?; + + Ok(new_token) +} + +// Fetch a specific form definition by its ID +pub fn get_form_definition(conn: &Connection, form_id: &str) -> AnyhowResult> { + let mut stmt = conn + .prepare("SELECT id, name, fields FROM forms WHERE id = ?1") + .context("Failed to prepare statement for getting form definition")?; + + let form_option = stmt + .query_row(params![form_id], |row| { + let id: String = row.get(0)?; + let name: String = row.get(1)?; + let fields_str: String = row.get(2)?; + + // Ensure fields can be parsed as valid JSON Value + let fields: serde_json::Value = serde_json::from_str(&fields_str).map_err(|e| { + // Log clearly that this is a data integrity issue + log::error!( + "Database integrity error: Failed to parse 'fields' JSON for form_id {}: {}. Content: '{}'", + id, e, fields_str // Log content if not too large/sensitive + ); + rusqlite::Error::FromSqlConversionFailure( + 2, + rusqlite::types::Type::Text, + Box::new(e), + ) + })?; + + // **Basic check**: Ensure fields is an array (common pattern for form definitions) + if !fields.is_array() { + log::error!( + "Database integrity error: 'fields' column for form_id {} is not a JSON array.", + id + ); + return Err(rusqlite::Error::FromSqlConversionFailure( + 2, + rusqlite::types::Type::Text, + "Form fields definition is not a valid JSON array".into(), + )); + } + + Ok(models::Form { + id: Some(id), + name, + fields, + }) + }) + .optional() // Handle case where form_id doesn't exist + .context(format!( + "Failed to execute query for form definition with id {}", + form_id + ))?; + + Ok(form_option) +} diff --git a/src/handlers.rs b/src/handlers.rs new file mode 100644 index 0000000..05bd313 --- /dev/null +++ b/src/handlers.rs @@ -0,0 +1,746 @@ +// src/handlers.rs +use crate::auth::Auth; +use crate::models::{Form, LoginCredentials, LoginResponse, Submission}; +use actix_web::{web, Error as ActixWebError, HttpResponse, Responder, Result as ActixResult}; +use anyhow::Context; // Import anyhow::Context for error chaining +use log; +use regex::Regex; // For pattern validation +use rusqlite::{params, Connection}; +use serde_json::{json, Map, Value as JsonValue}; // Alias for clarity +use std::collections::HashMap; +use std::error::Error as StdError; +use std::sync::{Arc, Mutex}; +use uuid::Uuid; + +// --- Helper Function for Database Access --- + +// Gets a database connection from the request data, handling lock errors consistently. +fn get_db_conn( + db: &web::Data>>, +) -> Result, ActixWebError> { + db.lock().map_err(|poisoned| { + log::error!("Database mutex poisoned: {}", poisoned); + actix_web::error::ErrorInternalServerError("Internal database error (mutex lock)") + }) +} + +// --- Helper Function for Validation --- + +/// Validates submission data against the form field definitions with enhanced checks. +/// +/// Expected field definition properties: +/// - `name`: string (required) +/// - `type`: string (e.g., "string", "number", "boolean", "email", "url", "object", "array") (required) +/// - `required`: boolean (optional, default: false) +/// - `maxLength`: number (for "string" type) +/// - `minLength`: number (for "string" type) +/// - `min`: number (for "number" type) +/// - `max`: number (for "number" type) +/// - `pattern`: string (regex for "string", "email", "url" types) +/// +/// Returns `Ok(())` if valid, or `Err(JsonValue)` containing validation errors. +fn validate_submission_against_definition( + submission_data: &JsonValue, + form_definition_fields: &JsonValue, +) -> Result<(), JsonValue> { + let mut errors: HashMap = HashMap::new(); + + // Ensure 'fields' in the definition is a JSON array + let field_definitions = match form_definition_fields.as_array() { + Some(defs) => defs, + None => { + log::error!( + "Form definition 'fields' is not a JSON array. Def: {:?}", + form_definition_fields + ); + errors.insert( + "_internal".to_string(), + "Invalid form definition format (not an array)".to_string(), + ); + return Err(json!({ "validation_errors": errors })); + } + }; + + // Ensure the submission data is a JSON object + let data_map = match submission_data.as_object() { + Some(map) => map, + None => { + errors.insert( + "_submission".to_string(), + "Submission data must be a JSON object".to_string(), + ); + return Err(json!({ "validation_errors": errors })); + } + }; + + // Build a map of valid field names to their definitions from the definition for quick lookup + let defined_field_names: HashMap> = field_definitions + .iter() + .filter_map(|val| val.as_object()) + .filter_map(|def| { + def.get("name") + .and_then(JsonValue::as_str) + .map(|name| (name.to_string(), def)) + }) + .collect(); + + // 1. Check for submitted fields that are NOT in the definition + for submitted_key in data_map.keys() { + if !defined_field_names.contains_key(submitted_key) { + errors.insert( + submitted_key.clone(), + "Unexpected field submitted".to_string(), + ); + } + } + // Exit early if unexpected fields were found + if !errors.is_empty() { + log::warn!("Submission validation failed: Unexpected fields submitted."); + return Err(json!({ "validation_errors": errors })); + } + + // 2. Iterate through each field definition and validate corresponding submitted data + for (field_name, field_def) in &defined_field_names { + // Extract properties using helper functions for clarity + let field_type = field_def + .get("type") + .and_then(JsonValue::as_str) + .unwrap_or("string"); // Default to "string" if type is missing or not a string + let is_required = field_def + .get("required") + .and_then(JsonValue::as_bool) + .unwrap_or(false); // Default to false if required is missing or not a boolean + let min_length = field_def.get("minLength").and_then(JsonValue::as_u64); + let max_length = field_def.get("maxLength").and_then(JsonValue::as_u64); + let min_value = field_def.get("min").and_then(JsonValue::as_f64); // Use f64 for flexibility + let max_value = field_def.get("max").and_then(JsonValue::as_f64); + let pattern = field_def.get("pattern").and_then(JsonValue::as_str); + + match data_map.get(field_name) { + Some(submitted_value) if !submitted_value.is_null() => { + // Field is present and not null, perform type and constraint checks + let mut type_error = None; + let mut constraint_errors = vec![]; + + match field_type { + "string" | "email" | "url" => { + if let Some(s) = submitted_value.as_str() { + if let Some(min) = min_length { + if (s.chars().count() as u64) < min { + // Use chars().count() for UTF-8 correctness + constraint_errors + .push(format!("Must be at least {} characters long", min)); + } + } + if let Some(max) = max_length { + if (s.chars().count() as u64) > max { + constraint_errors.push(format!( + "Must be no more than {} characters long", + max + )); + } + } + if let Some(pat) = pattern { + // Consider caching compiled Regex if performance is critical + // and patterns are reused frequently across requests. + match Regex::new(pat) { + Ok(re) => { + if !re.is_match(s) { + constraint_errors.push(format!("Does not match required pattern")); + } + } + Err(e) => log::warn!("Invalid regex pattern '{}' in form definition for field '{}': {}", pat, field_name, e), // Log regex compilation error + } + } + // Specific checks for email/url + if field_type == "email" { + // Basic email regex (adjust for stricter needs or use a validation crate) + // This regex is very basic and allows many technically invalid addresses. + // Consider crates like `validator` for more robust validation. + let email_regex = + Regex::new(r"^[^\s@]+@[^\s@]+\.[^\s@]+$").unwrap(); // Safe unwrap for known valid regex + if !email_regex.is_match(s) { + constraint_errors + .push("Must be a valid email address".to_string()); + } + } + if field_type == "url" { + // Basic URL check (consider `url` crate for robustness) + if url::Url::parse(s).is_err() { + constraint_errors.push("Must be a valid URL".to_string()); + } + } + } else { + type_error = Some(format!("Expected a string for '{}'", field_name)); + } + } + "number" => { + // Use as_f64 for flexibility (handles integers and floats) + if let Some(num) = submitted_value.as_f64() { + if let Some(min) = min_value { + if num < min { + constraint_errors.push(format!("Must be at least {}", min)); + } + } + if let Some(max) = max_value { + if num > max { + constraint_errors.push(format!("Must be no more than {}", max)); + } + } + } else { + type_error = Some(format!("Expected a number for '{}'", field_name)); + } + } + "boolean" => { + if !submitted_value.is_boolean() { + type_error = Some(format!( + "Expected a boolean (true/false) for '{}'", + field_name + )); + } + } + "object" => { + if !submitted_value.is_object() { + type_error = + Some(format!("Expected a JSON object for '{}'", field_name)); + } + // TODO: Could add deeper validation for object structure here if needed based on definition + } + "array" => { + if !submitted_value.is_array() { + type_error = + Some(format!("Expected a JSON array for '{}'", field_name)); + } + // TODO: Could add validation for array elements here if needed based on definition + } + _ => { + // Log unsupported types during development/debugging if necessary + log::trace!( + "Unsupported field type '{}' encountered during validation for field '{}'. Treating as valid.", + field_type, + field_name + ); + // Assume valid if type is not specifically handled or unknown + } + } + + // Record errors found for this field + if let Some(err) = type_error { + errors.insert(field_name.clone(), err); + } else if !constraint_errors.is_empty() { + // Combine multiple constraint errors if necessary + errors.insert(field_name.clone(), constraint_errors.join("; ")); + } + } // End check for present and non-null value + Some(_) => { + // Value is present but explicitly null (e.g., "fieldName": null) + if is_required { + errors.insert( + field_name.clone(), + "This field is required and cannot be null".to_string(), + ); + } + // Otherwise, null is considered a valid (empty) value for non-required fields + } + None => { + // Field is missing entirely from the submission object + if is_required { + errors.insert(field_name.clone(), "This field is required".to_string()); + } + // Missing is valid for non-required fields + } + } // End match data_map.get(field_name) + } // End loop through field definitions + + // Check if any errors were collected + if errors.is_empty() { + Ok(()) // Validation passed + } else { + log::info!( + "Submission validation failed with {} error(s).", // Log only the count for brevity + errors.len() + ); + // Return a JSON object containing the specific validation errors + Err(json!({ "validation_errors": errors })) + } +} + +// Helper function to convert anyhow::Error to actix_web::Error +fn anyhow_to_actix_error(e: anyhow::Error) -> ActixWebError { + actix_web::error::ErrorInternalServerError(e.to_string()) +} + +// --- Public Handlers --- + +// POST /login +pub async fn login( + db: web::Data>>, + creds: web::Json, +) -> ActixResult { + let db_conn = db.clone(); // Clone Arc for use in web::block + let username = creds.username.clone(); + let password = creds.password.clone(); + + // Wrap the blocking database operations in web::block + let auth_result = web::block(move || { + let conn = db_conn + .lock() + .map_err(|_| anyhow::anyhow!("Database mutex poisoned during login lock"))?; + crate::db::authenticate_user(&conn, &username, &password) + }) + .await + .map_err(|e| { + log::error!("web::block error during authentication: {:?}", e); + actix_web::error::ErrorInternalServerError("Authentication process failed (blocking error)") + })? + .map_err(anyhow_to_actix_error)?; + + match auth_result { + Some(user_data) => { + let db_conn_token = db.clone(); // Clone Arc again for token generation + let user_id = user_data.id.clone(); + + // Generate and store a new token within web::block + let token = web::block(move || { + let conn = db_conn_token + .lock() + .map_err(|_| anyhow::anyhow!("Database mutex poisoned during token lock"))?; + crate::db::generate_and_set_token_for_user(&conn, &user_id) + }) + .await + .map_err(|e| { + log::error!("web::block error during token generation: {:?}", e); + actix_web::error::ErrorInternalServerError( + "Failed to complete login (token generation blocking error)", + ) + })? + .map_err(anyhow_to_actix_error)?; + + log::info!("Login successful for user_id: {}", user_data.id); + Ok(HttpResponse::Ok().json(LoginResponse { token })) + } + None => { + log::warn!("Login failed for username: {}", creds.username); + // Return 401 Unauthorized for failed login attempts + Err(actix_web::error::ErrorUnauthorized( + "Invalid username or password", + )) + } + } +} + +// POST /logout +pub async fn logout( + db: web::Data>>, + auth: Auth, // Requires authentication (extracts user_id from token) +) -> ActixResult { + log::info!("User {} requesting logout", auth.user_id); + let db_conn = db.clone(); + let user_id = auth.user_id.clone(); + + // Invalidate the token in the database within web::block + web::block(move || { + let conn = db_conn + .lock() + .map_err(|_| anyhow::anyhow!("Database mutex poisoned during logout lock"))?; + crate::db::invalidate_token(&conn, &user_id) + }) + .await + .map_err(|e| { + let user_id = auth.user_id.clone(); // Clone user_id again after the move + log::error!( + "web::block error during logout for user {}: {:?}", + user_id, + e + ); + actix_web::error::ErrorInternalServerError("Logout failed (blocking error)") + })? + .map_err(anyhow_to_actix_error)?; + + log::info!("User {} logged out successfully", auth.user_id); + Ok(HttpResponse::Ok().json(json!({ "message": "Logged out successfully" }))) +} + +// POST /forms/{form_id}/submissions +pub async fn submit_form( + db: web::Data>>, + path: web::Path, // Extracts form_id from path + submission_payload: web::Json, // Expect arbitrary JSON payload +) -> ActixResult { + let form_id = path.into_inner(); + let submission_data = submission_payload.into_inner(); // Get the JSON data + + // --- Stage 1: Fetch form definition (Read-only, can use shared lock) --- + let form_definition = { + // Acquire lock temporarily for the read operation + let conn = get_db_conn(&db)?; + match crate::db::get_form_definition(&conn, &form_id) { + Ok(Some(form)) => form, + Ok(None) => { + log::warn!("Submission attempt for non-existent form_id: {}", form_id); + return Err(actix_web::error::ErrorNotFound("Form not found")); + } + Err(e) => { + log::error!("Failed to fetch form definition for {}: {:?}", form_id, e); + return Err(actix_web::error::ErrorInternalServerError( + "Could not retrieve form information", + )); + } + } + // Lock is released here when 'conn' goes out of scope + }; + + // --- Stage 2: Validate submission against definition (CPU-bound, no DB lock needed) --- + if let Err(validation_errors) = + validate_submission_against_definition(&submission_data, &form_definition.fields) + { + log::warn!( + "Submission validation failed for form_id {}. Errors: {:?}", // Log actual errors if needed (might be verbose) + form_id, + validation_errors + ); + // Return 400 Bad Request with validation error details + return Ok(HttpResponse::BadRequest().json(validation_errors)); + } + + // --- Stage 3: Serialize validated data and Insert submission (Write operation, use web::block) --- + let submission_json = match serde_json::to_string(&submission_data) { + Ok(json_string) => json_string, + Err(e) => { + log::error!( + "Failed to serialize validated submission data for form {}: {}", + form_id, + e + ); + return Err(actix_web::error::ErrorInternalServerError( + "Failed to process submission data internally", + )); + } + }; + + let db_conn_write = db.clone(); // Clone Arc for the blocking operation + let form_id_clone = form_id.clone(); // Clone for closure + let submission_id = Uuid::new_v4().to_string(); // Generate unique ID for the submission + let submission_id_clone = submission_id.clone(); // Clone for closure + + web::block(move || { + let conn = db_conn_write.lock().map_err(|_| { + anyhow::anyhow!("Database mutex poisoned during submission insert lock") + })?; + conn.execute( + "INSERT INTO submissions (id, form_id, data) VALUES (?1, ?2, ?3)", + params![submission_id_clone, form_id_clone, submission_json], + ) + .context(format!( + "Failed to insert submission for form {}", + form_id_clone + )) + .map_err(anyhow::Error::from) + }) + .await + .map_err(|e| { + log::error!( + "web::block error during submission insertion for form {}: {:?}", + form_id, + e + ); + actix_web::error::ErrorInternalServerError("Failed to save submission (blocking error)") + })? + .map_err(anyhow_to_actix_error)?; + + log::info!( + "Successfully inserted submission {} for form_id {}", + submission_id, + form_id + ); + // Return 200 OK with the new submission ID + Ok(HttpResponse::Ok().json(json!({ "submission_id": submission_id }))) +} + +// --- Protected Handlers (Require Auth) --- + +// POST /forms +pub async fn create_form( + db: web::Data>>, + auth: Auth, // Authentication check via Auth extractor + form_payload: web::Json, +) -> ActixResult { + log::info!( + "User {} attempting to create form: {}", + auth.user_id, + form_payload.name + ); + + let mut form = form_payload.into_inner(); + // Generate a new UUID for the form if not provided (or overwrite if provided) + let form_id = form.id.unwrap_or_else(|| Uuid::new_v4().to_string()); + form.id = Some(form_id.clone()); // Ensure the form object has the ID + + // Basic structural validation: Ensure 'fields' is a JSON array before serialization/saving + if !form.fields.is_array() { + log::error!( + "User {} attempted to create form '{}' ('{}') where 'fields' is not a JSON array.", + auth.user_id, + form.name, + form_id + ); + return Err(actix_web::error::ErrorBadRequest( + "Form 'fields' must be a valid JSON array.", + )); + } + // TODO: Add deeper validation of the 'fields' structure itself if needed + // e.g., check if each element in 'fields' is an object with 'name' and 'type'. + + // Serialize the fields part to JSON string for DB storage + let fields_json = match serde_json::to_string(&form.fields) { + Ok(json_str) => json_str, + Err(e) => { + log::error!( + "Failed to serialize form fields for form '{}' ('{}') by user {}: {}", + form.name, + form_id, + auth.user_id, + e + ); + return Err(actix_web::error::ErrorInternalServerError( + "Failed to process form fields internally", + )); + } + }; + + // Clone data needed for the blocking database operation + let db_conn = db.clone(); + // let form_id = form_id; // Already have it from above + let form_name = form.name.clone(); + let user_id = auth.user_id.clone(); // For logging inside block if needed + + // Insert the form using web::block for the blocking DB write + web::block(move || { + let conn = db_conn + .lock() + .map_err(|_| anyhow::anyhow!("Database mutex poisoned during form creation lock"))?; + conn.execute( + // Consider adding user_id to the forms table if forms are user-specific + "INSERT INTO forms (id, name, fields) VALUES (?1, ?2, ?3)", + params![form_id, form_name, fields_json], + ) + .context("Failed to insert new form into database") + .map_err(anyhow::Error::from) + }) + .await + .map_err(|e| { + log::error!( + "web::block error during form creation by user {}: {:?}", + auth.user_id, + e + ); + actix_web::error::ErrorInternalServerError("Failed to create form (blocking error)") + })? + .map_err(anyhow_to_actix_error)?; + + log::info!( + "Successfully created form '{}' with id {} by user {}", + form.name, + form.id.as_ref().unwrap(), // Safe unwrap as we set it + auth.user_id + ); + // Return 200 OK with the newly created form object (including its ID) + Ok(HttpResponse::Ok().json(form)) +} + +// GET /forms +pub async fn get_forms( + db: web::Data>>, + auth: Auth, // Requires authentication +) -> ActixResult { + log::info!("User {} requesting list of forms", auth.user_id); + let db_conn = db.clone(); + let user_id = auth.user_id.clone(); // Clone for logging context if needed inside block + + // Wrap DB query in web::block as it might be slow with many forms or complex parsing + let forms_result = web::block(move || { + let conn = db_conn + .lock() + .map_err(|_| anyhow::anyhow!("Database mutex poisoned during get_forms lock"))?; + + let mut stmt = conn + .prepare("SELECT id, name, fields FROM forms") + .context("Failed to prepare statement for getting forms")?; + + let forms_iter = stmt + .query_map([], |row| { + let id: String = row.get(0)?; + let name: String = row.get(1)?; + let fields_str: String = row.get(2)?; + + // Parse the 'fields' JSON string. If it fails, log the error and skip the row. + let fields: serde_json::Value = match serde_json::from_str(&fields_str) { + Ok(json_value) => json_value, + Err(e) => { + // Log the data integrity issue clearly + log::error!( + "DB Parse Error: Failed to parse 'fields' JSON for form id {}: {}. Skipping this form.", + id, e + ); + // Return a special error that `filter_map` below can catch, + // without failing the entire query_map. + // Using a specific rusqlite error type here is okay. + return Err(rusqlite::Error::FromSqlConversionFailure( + 2, // Column index + rusqlite::types::Type::Text, + Box::new(e) // Box the original error + )); + } + }; + + Ok(Form { id: Some(id), name, fields }) + }) + .context("Failed to execute query map for getting forms")?; + + // Collect results, filtering out rows that failed parsing WITHIN the block + let forms: Vec = forms_iter + .filter_map(|result| match result { + Ok(form) => Some(form), + Err(e) => { + // Error was already logged inside the query_map closure. + // We just filter out the failed row here. + log::warn!("Skipping a form row due to a processing error: {}", e); + None // Skip this row + } + }) + .collect(); + + Ok::<_, anyhow::Error>(forms) // Ensure block returns Result compatible with flattening + }) + .await + .map_err(|e| { + // Handle web::block error + log::error!("web::block error during get_forms for user {}: {:?}", user_id, e); + actix_web::error::ErrorInternalServerError("Failed to retrieve forms (blocking error)") + })? + .map_err(anyhow_to_actix_error)?; // Flatten Result, anyhow::Error>, BlockingError> + + log::debug!( + "Returning {} forms for user {}", + forms_result.len(), + auth.user_id + ); + Ok(HttpResponse::Ok().json(forms_result)) +} + +// GET /forms/{form_id}/submissions +pub async fn get_submissions( + db: web::Data>>, + auth: Auth, // Requires authentication + path: web::Path, // Extracts form_id from the path +) -> ActixResult { + let form_id = path.into_inner(); + log::info!( + "User {} requesting submissions for form_id: {}", + auth.user_id, + form_id + ); + + let db_conn = db.clone(); + let form_id_clone = form_id.clone(); + let user_id = auth.user_id.clone(); // Clone for logging context + + // Wrap DB queries (existence check + fetching submissions) in web::block + let submissions_result = web::block(move || { + let conn = db_conn + .lock() + .map_err(|_| anyhow::anyhow!("Database mutex poisoned during get_submissions lock"))?; + + // 1. Check if the form exists first + let form_exists: bool = match conn.query_row( + "SELECT EXISTS(SELECT 1 FROM forms WHERE id = ?1 LIMIT 1)", // Added LIMIT 1 for potential optimization + params![form_id_clone], + |row| row.get::<_, i32>(0), // sqlite returns 0 or 1 for EXISTS + ) { + Ok(count) => count == 1, + Err(rusqlite::Error::QueryReturnedNoRows) => false, // Should not happen with EXISTS, but handle defensively + Err(e) => return Err(anyhow::Error::from(e) // Propagate other DB errors + .context(format!("Failed check existence of form {}", form_id_clone))), + }; + + if !form_exists { + // Use Ok(None) to signal "form not found" to the calling async context + return Ok(None); + } + + // 2. If form exists, fetch its submissions + let mut stmt = conn.prepare( + "SELECT id, form_id, data, created_at FROM submissions WHERE form_id = ?1 ORDER BY created_at DESC", // Include created_at if needed + ) + .context(format!("Failed to prepare statement for getting submissions for form {}", form_id_clone))?; + + let submissions_iter = stmt + .query_map(params![form_id_clone], |row| { + let id: String = row.get(0)?; + let form_id_db: String = row.get(1)?; + let data_str: String = row.get(2)?; + // let created_at: String = row.get(3)?; // Example: If you fetch created_at + + // Parse the 'data' JSON string, handling potential errors + let data: serde_json::Value = match serde_json::from_str(&data_str) { + Ok(json_value) => json_value, + Err(e) => { + log::error!( + "DB Parse Error: Failed to parse 'data' JSON for submission_id {}: {}. Skipping.", + id, e + ); + // Return specific error for filter_map + return Err(rusqlite::Error::FromSqlConversionFailure( + 2, rusqlite::types::Type::Text, Box::new(e) + )); + } + }; + + Ok(Submission { id, form_id: form_id_db, data }) // Add created_at if fetched + }) + .context(format!("Failed to execute query map for getting submissions for form {}", form_id_clone))?; + + // Collect valid submissions, filtering out rows that failed parsing + let submissions: Vec = submissions_iter + .filter_map(|result| match result { + Ok(submission) => Some(submission), + Err(e) => { + log::warn!("Skipping a submission row due to processing error: {}", e); + None // Skip this row + } + }) + .collect(); + + Ok(Some(submissions)) // Indicate success with the (potentially empty) list of submissions + + }) + .await + .map_err(|e| { // Handle web::block error (cancellation, panic) + log::error!("web::block error during get_submissions for form {} by user {}: {:?}", form_id, user_id, e); + actix_web::error::ErrorInternalServerError("Failed to retrieve submissions (blocking error)") + })? + .map_err(anyhow_to_actix_error)?; // Flatten Result>, anyhow::Error>, BlockingError> + + // Process the result obtained from the web::block + match submissions_result { + Some(submissions) => { + // Form exists, return the found submissions (might be an empty list) + log::debug!( + "Returning {} submissions for form {} requested by user {}", + submissions.len(), + form_id, + auth.user_id + ); + Ok(HttpResponse::Ok().json(submissions)) + } + None => { + // Form was not found (signaled by Ok(None) from the block) + log::warn!( + "Attempt by user {} to get submissions for non-existent form_id: {}", + auth.user_id, + form_id + ); + Err(actix_web::error::ErrorNotFound("Form not found")) + } + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..79e36d6 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,167 @@ +// src/main.rs +use actix_cors::Cors; +use actix_files as fs; +use actix_web::{http::header, middleware::Logger, web, App, HttpServer}; // Added Logger explicitly +use dotenv::dotenv; +use log; +use std::env; +use std::io::Result as IoResult; // Alias for clarity +use std::process; +use std::sync::{Arc, Mutex}; + +// Import modules +mod auth; +mod db; +mod handlers; +mod models; + +#[actix_web::main] +async fn main() -> IoResult<()> { + dotenv().ok(); // Load .env file + + // Initialize logger (using RUST_LOG env var) + env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init(); + + // --- Configuration (Environment Variables) --- + // CRITICAL: Database URL is required + let database_url = env::var("DATABASE_URL").unwrap_or_else(|_| { + log::warn!("DATABASE_URL environment variable not set. Defaulting to 'form_data.db'."); + "form_data.db".to_string() + }); + // CRITICAL: Bind address is required + let bind_address = env::var("BIND_ADDRESS").unwrap_or_else(|_| { + log::warn!("BIND_ADDRESS environment variable not set. Defaulting to '127.0.0.1:8080'."); + "127.0.0.1:8080".to_string() + }); + // CRITICAL: Initial admin credentials (checked in db::init_db) + // let initial_admin_username = env::var("INITIAL_ADMIN_USERNAME").expect("Missing INITIAL_ADMIN_USERNAME"); + // let initial_admin_password = env::var("INITIAL_ADMIN_PASSWORD").expect("Missing INITIAL_ADMIN_PASSWORD"); + // OPTIONAL: Allowed origin for CORS + let allowed_origin = env::var("ALLOWED_ORIGIN").ok(); // Use ok() to make it optional + + log::info!(" --- Formies Backend Configuration ---"); + log::info!("Required Environment Variables:"); + log::info!(" - DATABASE_URL (Current: {})", database_url); + log::info!(" - BIND_ADDRESS (Current: {})", bind_address); + log::info!(" - INITIAL_ADMIN_USERNAME (Set during startup, MUST be present)"); + log::info!(" - INITIAL_ADMIN_PASSWORD (Set during startup, MUST be present)"); + log::info!("Optional Environment Variables:"); + if let Some(ref origin) = allowed_origin { + log::info!(" - ALLOWED_ORIGIN (Set: {})", origin); + } else { + log::warn!(" - ALLOWED_ORIGIN (Not Set): CORS will be restrictive, potentially blocking browser access. Set to your frontend URL (e.g., http://localhost:5173 or https://yourdomain.com)."); + } + log::info!(" - RUST_LOG (e.g., 'info,formies_be=debug')"); + log::info!(" --- End Configuration ---"); + + // Initialize database connection + let db_connection = match db::init_db(&database_url) { + Ok(conn) => conn, + Err(e) => { + // Specific check for missing admin credentials error + if e.to_string().contains("INITIAL_ADMIN_USERNAME") + || e.to_string().contains("INITIAL_ADMIN_PASSWORD") + { + log::error!("FATAL: {}", e); + log::error!("Please set the INITIAL_ADMIN_USERNAME and INITIAL_ADMIN_PASSWORD environment variables."); + } else { + log::error!( + "FATAL: Failed to initialize database at {}: {:?}", + database_url, + e + ); + } + process::exit(1); // Exit if DB initialization fails + } + }; + + // Wrap connection in Arc> for thread-safe sharing + let db_data = web::Data::new(Arc::new(Mutex::new(db_connection))); + + log::info!("Starting server at http://{}", bind_address); + + HttpServer::new(move || { + // Clone shared state for the closure + let db_data_clone = db_data.clone(); + let allowed_origin_clone = allowed_origin.clone(); + + // Configure CORS + let cors = match allowed_origin_clone { + Some(origin) => { + log::info!("Configuring CORS for specific origin: {}", origin); + Cors::default() + .allowed_origin(&origin) // Allow only the specified origin + .allowed_methods(vec!["GET", "POST", "PUT", "DELETE", "OPTIONS"]) + .allowed_headers(vec![ + header::AUTHORIZATION, + header::ACCEPT, + header::CONTENT_TYPE, + header::ORIGIN, // Add Origin header if needed + header::ACCESS_CONTROL_REQUEST_METHOD, + header::ACCESS_CONTROL_REQUEST_HEADERS, + ]) + .supports_credentials() + .max_age(3600) + } + None => { + // Default restrictive CORS: No origin allowed explicitly. + // This will likely block browser requests unless the browser and server are on the same origin. + log::warn!("CORS is configured restrictively: No ALLOWED_ORIGIN set."); + Cors::default() // No allowed_origin set + .allowed_methods(vec!["GET", "POST", "PUT", "DELETE", "OPTIONS"]) + .allowed_headers(vec![ + header::AUTHORIZATION, + header::ACCEPT, + header::CONTENT_TYPE, + header::ORIGIN, + header::ACCESS_CONTROL_REQUEST_METHOD, + header::ACCESS_CONTROL_REQUEST_HEADERS, + ]) + .supports_credentials() + .max_age(3600) + // DO NOT use allow_any_origin() unless you fully understand the security implications. + } + }; + + App::new() + .wrap(cors) // Apply CORS middleware + .wrap(Logger::default()) // Add request logging (default format) + .app_data(db_data_clone) // Share database connection pool + // --- API Routes --- + .service( + web::scope("/api") // Group API routes under /api + // --- Public Routes --- + .route("/login", web::post().to(handlers::login)) + .route( + "/forms/{form_id}/submissions", + web::post().to(handlers::submit_form), + ) + // --- Protected Routes (using Auth extractor) --- + .route("/logout", web::post().to(handlers::logout)) // Added logout + .route("/forms", web::post().to(handlers::create_form)) + .route("/forms", web::get().to(handlers::get_forms)) + .route( + "/forms/{form_id}/submissions", + web::get().to(handlers::get_submissions), + ), + ) + // --- Static Files (Serve Frontend - Optional) --- + // Assumes frontend build output is in ../frontend/dist + // Register this LAST to avoid conflicts with API routes + .service( + fs::Files::new("/", "../frontend/dist/") + .index_file("index.html") + .use_last_modified(true) + // Optional: Add a fallback to index.html for SPA routing + .default_handler( + fs::NamedFile::open("../frontend/dist/index.html").unwrap_or_else(|_| { + log::error!("Fallback file not found: ../frontend/dist/index.html"); + process::exit(1); // Exit if fallback file is missing + }), // Handle error explicitly + ), + ) + }) + .bind(&bind_address)? + .run() + .await +} diff --git a/src/models.rs b/src/models.rs new file mode 100644 index 0000000..19f584e --- /dev/null +++ b/src/models.rs @@ -0,0 +1,139 @@ +// src/models.rs +use serde::{Deserialize, Serialize}; +// Consider adding chrono for DateTime types if needed in responses +// use chrono::{DateTime, Utc}; + +// Represents the structure for defining a form +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct Form { + #[serde(skip_serializing_if = "Option::is_none")] + pub id: Option, + pub name: String, + /// Stores the structure defining the form fields. + /// Expected to be a JSON array of field definition objects. + /// Example field definition object: + /// ```json + /// { + /// "name": "email", // String, required: Unique identifier for the field + /// "type": "email", // String, required: "string", "number", "boolean", "email", "url", "object", "array" + /// "label": "Email Address", // String, optional: User-friendly label + /// "required": true, // Boolean, optional (default: false): If the field must have a value + /// "placeholder": "you@example.com", // String, optional: Placeholder text + /// "minLength": 5, // Number, optional: Minimum length for strings + /// "maxLength": 100, // Number, optional: Maximum length for strings + /// "min": 0, // Number, optional: Minimum value for numbers + /// "max": 100, // Number, optional: Maximum value for numbers + /// "pattern": "^\\S+@\\S+\\.\\S+$" // String, optional: Regex pattern for strings (e.g., email, url types might use this implicitly or explicitly) + /// // Add other properties like "options" for select/radio, etc. + /// } + /// ``` + pub fields: serde_json::Value, + // Optional: Add created_at if needed in API responses + // pub created_at: Option>, +} + +// Represents a single submission for a specific form +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct Submission { + pub id: String, + pub form_id: String, + /// Stores the data submitted by the user. + /// Expected to be a JSON object where keys are field names (`name`) from the form definition's `fields` array. + /// Example: `{ "email": "user@example.com", "age": 30 }` + pub data: serde_json::Value, + // Optional: Add created_at if needed in API responses + // pub created_at: Option>, +} + +// Used for the /login endpoint request body +#[derive(Debug, Serialize, Deserialize)] +pub struct LoginCredentials { + pub username: String, + pub password: String, +} + +// Used for the /login endpoint response body +#[derive(Debug, Serialize, Deserialize)] +pub struct LoginResponse { + pub token: String, // The session token (UUID) +} + +// Used internally to represent a user fetched from the DB for authentication check +// Not serialized, only used within db.rs and handlers.rs +#[derive(Debug)] +pub struct UserAuthData { + pub id: String, + pub hashed_password: String, + // Note: Token and expiry are handled separately and not needed in this specific struct +} + +// --- Custom Application Error (Optional but Recommended for Consistency) --- +// Although not fully integrated in this pass to minimize changes, +// this shows the structure for future improvement. + +// use actix_web::{ResponseError, http::StatusCode}; +// use std::fmt; + +// #[derive(Debug)] +// pub enum AppError { +// DatabaseError(anyhow::Error), +// ConfigError(String), +// ValidationError(serde_json::Value), // Store the validation errors JSON +// NotFound(String), +// Unauthorized(String), +// InternalError(String), +// BlockingError(String), +// } + +// impl fmt::Display for AppError { +// fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { +// match self { +// AppError::DatabaseError(e) => write!(f, "Database error: {}", e), +// AppError::ConfigError(s) => write!(f, "Configuration error: {}", s), +// AppError::ValidationError(_) => write!(f, "Validation failed"), +// AppError::NotFound(s) => write!(f, "Not found: {}", s), +// AppError::Unauthorized(s) => write!(f, "Unauthorized: {}", s), +// AppError::InternalError(s) => write!(f, "Internal server error: {}", s), +// AppError::BlockingError(s) => write!(f, "Blocking operation error: {}", s), +// } +// } +// } + +// impl ResponseError for AppError { +// fn status_code(&self) -> StatusCode { +// match self { +// AppError::DatabaseError(_) => StatusCode::INTERNAL_SERVER_ERROR, +// AppError::ConfigError(_) => StatusCode::INTERNAL_SERVER_ERROR, +// AppError::ValidationError(_) => StatusCode::BAD_REQUEST, +// AppError::NotFound(_) => StatusCode::NOT_FOUND, +// AppError::Unauthorized(_) => StatusCode::UNAUTHORIZED, +// AppError::InternalError(_) => StatusCode::INTERNAL_SERVER_ERROR, +// AppError::BlockingError(_) => StatusCode::INTERNAL_SERVER_ERROR, +// } +// } + +// fn error_response(&self) -> HttpResponse { +// let status = self.status_code(); +// let error_json = match self { +// AppError::ValidationError(errors) => errors.clone(), +// // Provide a generic error structure for others +// _ => json!({ "error": status.canonical_reason().unwrap_or("Unknown Error"), "message": self.to_string() }), +// }; + +// HttpResponse::build(status).json(error_json) +// } +// } + +// // Implement From traits to convert other errors into AppError easily +// impl From for AppError { +// fn from(err: anyhow::Error) -> Self { +// // Basic conversion, could add more context analysis here +// AppError::DatabaseError(err) +// } +// } +// impl From for AppError { +// fn from(err: actix_web::error::BlockingError) -> Self { +// AppError::BlockingError(err.to_string()) +// } +//} +// // Add From, From, etc. as needed