commit c5632f6a37f502f26b5f0850aad5c7431b237ec3 Author: tdv Date: Sun Aug 31 22:42:08 2025 +0300 first commit, i i have no idea what i have done diff --git a/.env b/.env new file mode 100644 index 0000000..91ddd77 --- /dev/null +++ b/.env @@ -0,0 +1,16 @@ +VAULT_ADDR=http://vault:8200 +VAULT_TOKEN=root +VAULT_KV_PATH=kv/data/snoop +CONFIG_MODE=dev +DB_DSN=postgres://snoop:example@postgres:5432/snoop?sslmode=disable +MINIO_ENDPOINT=minio:9000 +MINIO_ACCESS_KEY=minioadmin +MINIO_SECRET_KEY=minioadmin +MINIO_USE_SSL=false +MINIO_RECORDS_BUCKET=records +MINIO_LIVESTREAM_BUCKET=livestream +MINIO_PRESIGN_TTL_SECONDS=900 +JWT_SECRET=devsupersecret + +ADMIN_USERNAME=admin +ADMIN_PASSWORD=admin \ No newline at end of file diff --git a/.vs/NewSmoop/FileContentIndex/d6ab3d63-4ffd-4964-a3b7-ccd5e1d776eb.vsidx b/.vs/NewSmoop/FileContentIndex/d6ab3d63-4ffd-4964-a3b7-ccd5e1d776eb.vsidx new file mode 100644 index 0000000..181079b Binary files /dev/null and b/.vs/NewSmoop/FileContentIndex/d6ab3d63-4ffd-4964-a3b7-ccd5e1d776eb.vsidx differ diff --git a/.vs/NewSmoop/v17/.wsuo b/.vs/NewSmoop/v17/.wsuo new file mode 100644 index 0000000..7187888 Binary files /dev/null and b/.vs/NewSmoop/v17/.wsuo differ diff --git a/.vs/NewSmoop/v17/DocumentLayout.json b/.vs/NewSmoop/v17/DocumentLayout.json new file mode 100644 index 0000000..31ebcaf --- /dev/null +++ b/.vs/NewSmoop/v17/DocumentLayout.json @@ -0,0 +1,41 @@ +{ + "Version": 1, + "WorkspaceRootPath": "D:\\sourses\\NewSmoop\\", + "Documents": [ + { + "AbsoluteMoniker": "D:0:0:{A2FE74E1-B743-11D0-AE1A-00A0C90FFFC3}|\u003CMiscFiles\u003E|D:\\sourses\\NewSmoop\\server\\cmd\\api\\main.go||{3B902123-F8A7-4915-9F01-361F908088D0}", + "RelativeMoniker": "D:0:0:{A2FE74E1-B743-11D0-AE1A-00A0C90FFFC3}|\u003CMiscFiles\u003E|solutionrelative:server\\cmd\\api\\main.go||{3B902123-F8A7-4915-9F01-361F908088D0}" + } + ], + "DocumentGroupContainers": [ + { + "Orientation": 0, + "VerticalTabListWidth": 256, + "DocumentGroups": [ + { + "DockedWidth": 200, + "SelectedChildIndex": 1, + "Children": [ + { + "$type": "Bookmark", + "Name": "ST:0:0:{1c4feeaa-4718-4aa9-859d-94ce25d182ba}" + }, + { + "$type": "Document", + "DocumentIndex": 0, + "Title": "main.go", + "DocumentMoniker": "D:\\sourses\\NewSmoop\\server\\cmd\\api\\main.go", + "RelativeDocumentMoniker": "server\\cmd\\api\\main.go", + "ToolTip": "D:\\sourses\\NewSmoop\\server\\cmd\\api\\main.go", + "RelativeToolTip": "server\\cmd\\api\\main.go", + "ViewState": "AgIAABIAAAAAAAAAAAAAAAsAAAABAAAAAAAAAA==", + "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.001001|", + "WhenOpened": "2025-08-19T11:50:45.464Z", + "EditorCaption": "" + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/.vs/ProjectSettings.json b/.vs/ProjectSettings.json new file mode 100644 index 0000000..866f1e1 --- /dev/null +++ b/.vs/ProjectSettings.json @@ -0,0 +1,3 @@ +{ + "CurrentProjectSetting": null +} \ No newline at end of file diff --git a/.vs/VSWorkspaceState.json b/.vs/VSWorkspaceState.json new file mode 100644 index 0000000..7a60152 --- /dev/null +++ b/.vs/VSWorkspaceState.json @@ -0,0 +1,11 @@ +{ + "ExpandedNodes": [ + "", + "\\server", + "\\server\\cmd", + "\\server\\cmd\\api", + "\\server\\internal" + ], + "SelectedNode": "\\server\\cmd\\api\\main.go", + "PreviewInSolutionExplorer": false +} \ No newline at end of file diff --git a/.vs/slnx.sqlite b/.vs/slnx.sqlite new file mode 100644 index 0000000..9c6a303 Binary files /dev/null and b/.vs/slnx.sqlite differ diff --git a/certs/gen-wildcard-nipio.sh b/certs/gen-wildcard-nipio.sh new file mode 100644 index 0000000..937a37e --- /dev/null +++ b/certs/gen-wildcard-nipio.sh @@ -0,0 +1,153 @@ +#!/usr/bin/env bash +set -euo pipefail + +# === Config (you can override via env or first arg) === +DOMAIN="${1:-${DOMAIN:-192.168.205.130.nip.io}}" +BITS="${BITS:-4096}" +DAYS_CA="${DAYS_CA:-3650}" # ~10 years for the CA +DAYS_CERT="${DAYS_CERT:-397}" # keep server cert within common client limits +WORKDIR="${WORKDIR:-ssl-${DOMAIN}}" + +echo ">> Generating Root CA and wildcard cert for domain: ${DOMAIN}" +echo ">> Work directory: ${WORKDIR}" +mkdir -p "${WORKDIR}" +cd "${WORKDIR}" + +# Tighten key perms by default +umask 077 + +# --- CA OpenSSL config --- +cat > ca.cnf <<'EOF' +[ ca ] +default_ca = CA_default + +[ CA_default ] +dir = . +new_certs_dir = $dir/newcerts +database = $dir/index.txt +serial = $dir/serial +private_key = $dir/ca.key.pem +certificate = $dir/ca.cert.pem +default_md = sha256 +policy = policy_any +copy_extensions = copy + +[ policy_any ] +commonName = supplied +countryName = optional +stateOrProvinceName = optional +localityName = optional +organizationName = optional +organizationalUnitName = optional +emailAddress = optional + +[ req ] +default_bits = 4096 +prompt = no +default_md = sha256 +x509_extensions = v3_ca +distinguished_name = req_dn +string_mask = utf8only + +[ req_dn ] +CN = Example Local Root CA + +[ v3_ca ] +basicConstraints = critical, CA:true, pathlen:0 +keyUsage = critical, keyCertSign, cRLSign +subjectKeyIdentifier = hash +authorityKeyIdentifier = keyid:always,issuer +EOF + +mkdir -p newcerts +: > index.txt +echo 1000 > serial + +# --- Generate CA key & self-signed cert (RSA-4096) --- +echo ">> Creating CA key..." +openssl genrsa -out ca.key.pem "${BITS}" >/dev/null 2>&1 +chmod 600 ca.key.pem + +echo ">> Creating CA certificate..." +openssl req -config ca.cnf -key ca.key.pem -new -x509 -days "${DAYS_CA}" -sha256 -out ca.cert.pem + +# --- Server/OpenSSL config with SAN --- +cat > server.cnf <> Creating server key..." +openssl genrsa -out "${DOMAIN}.key" "${BITS}" >/dev/null 2>&1 +chmod 600 "${DOMAIN}.key" + +echo ">> Creating CSR..." +openssl req -new -key "${DOMAIN}.key" -out "${DOMAIN}.csr.pem" -config server.cnf + +# --- Extensions for final certificate (keep SAN/EKU consistent) --- +cat > cert_ext.cnf <> Signing server certificate with CA..." +openssl x509 -req \ + -in "${DOMAIN}.csr.pem" \ + -CA ca.cert.pem -CAkey ca.key.pem -CAcreateserial \ + -out "${DOMAIN}.crt" \ + -days "${DAYS_CERT}" -sha256 \ + -extfile cert_ext.cnf -extensions v3_server + +# --- Produce handy bundles --- +# Leaf only (typical when client already trusts the CA) +cp "${DOMAIN}.crt" "${DOMAIN}.leaf.pem" + +# Leaf + CA (some servers/clients expect to see the issuing cert) +cat "${DOMAIN}.crt" ca.cert.pem > "${DOMAIN}.chain.pem" + +# Optional PFX for systems that prefer it (no password) +openssl pkcs12 -export -out "${DOMAIN}.pfx" -inkey "${DOMAIN}.key" -in "${DOMAIN}.crt" -certfile ca.cert.pem -passout pass: + +# --- Verify --- +echo ">> Verifying certificate chain..." +openssl verify -CAfile ca.cert.pem "${DOMAIN}.crt" + +echo +echo "=== Done ===" +echo "Files created in ${WORKDIR}:" +printf " - Root CA: ca.key.pem (KEEP SECRET), ca.cert.pem (import into trust store)\n" +printf " - Server key: %s.key (KEEP SECRET)\n" "${DOMAIN}" +printf " - Server cert: %s.crt\n" "${DOMAIN}" +printf " - Chain: %s.chain.pem (leaf + CA)\n" "${DOMAIN}" +printf " - CSR: %s.csr.pem\n" "${DOMAIN}" +printf " - PFX: %s.pfx (no password)\n" "${DOMAIN}" +echo +echo "Tip: import ca.cert.pem into your OS/browser trust store; point your server at:" +echo " key=${DOMAIN}.key" +echo " cert=${DOMAIN}.crt (or ${DOMAIN}.chain.pem if it wants a chain)" diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000..0af55e9 --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,108 @@ +services: + postgres: + image: postgres:16 + environment: + POSTGRES_PASSWORD: example + POSTGRES_DB: snoop + POSTGRES_USER: snoop + ports: + - "5432:5432" + volumes: + - pgdata:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"] + interval: 10s + timeout: 5s + retries: 10 + networks: + - snoopBack + + minio: + image: minio/minio:latest + command: server /data --console-address ":9001" + environment: + MINIO_ROOT_USER: minioadmin + MINIO_ROOT_PASSWORD: minioadmin + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/ready"] + interval: 5s + timeout: 3s + retries: 5 + volumes: + - miniodata:/data + ports: # console :9001 is handy during dev + - "9000:9000" + - "9001:9001" + networks: + - snoopBack + + vault: + image: hashicorp/vault:1.16 + environment: + VAULT_API_ADDR: http://0.0.0.0:8200 + VAULT_TOKEN: root + ports: + - 8200:8200 + cap_add: + - IPC_LOCK + volumes: + - vault-data:/vault/data + networks: + - snoopBack + + snoop-api: + build: + context: ./server + dockerfile: Dockerfile + args: + APP_DIR: ${API_APP_DIR:-./cmd/api} + environment: + VAULT_ADDR: "http://vault:8200" + VAULT_TOKEN: "root" + VAULT_KV_PATH: "kv/data/snoop" + MINIO_ENDPOINT: "http://minio:9000" + env_file: + - .env + depends_on: + postgres: + condition: service_healthy + minio: + condition: service_healthy + networks: + - snoopBack + - proxy + + + + web: + build: + context: ./management-ui + dockerfile: Dockerfile + environment: + VITE_API_URL: /api + networks: + - proxy + + nginx: + image: nginx:1.27-alpine + depends_on: + - web + - snoop-api + ports: + - "80:80" + volumes: + - ./nginx/dev.conf:/etc/nginx/conf.d/default.conf:ro,Z + networks: + - proxy + + + +volumes: + pgdata: + miniodata: + vault-data: + +networks: + proxy: + external: true + snoopBack: diff --git a/management-ui/.dockerignore b/management-ui/.dockerignore new file mode 100644 index 0000000..f8f04d0 --- /dev/null +++ b/management-ui/.dockerignore @@ -0,0 +1,7 @@ +node_modules +dist +.git +.gitignore +npm-debug.log +.vscode +.vs \ No newline at end of file diff --git a/management-ui/.gitignore b/management-ui/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/management-ui/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/management-ui/.vscode/extensions.json b/management-ui/.vscode/extensions.json new file mode 100644 index 0000000..a7cea0b --- /dev/null +++ b/management-ui/.vscode/extensions.json @@ -0,0 +1,3 @@ +{ + "recommendations": ["Vue.volar"] +} diff --git a/management-ui/Dockerfile b/management-ui/Dockerfile new file mode 100644 index 0000000..323da87 --- /dev/null +++ b/management-ui/Dockerfile @@ -0,0 +1,16 @@ +FROM node:22-alpine +WORKDIR /app + +# Install deps +COPY package*.json ./ +RUN npm ci + +# Copy source +COPY . . + +# Vite dev server +ENV HOST=0.0.0.0 +ENV PORT=5173 +EXPOSE 5173 +ENV VITE_API_URL=/api +CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0", "--port", "5173"] \ No newline at end of file diff --git a/management-ui/README.md b/management-ui/README.md new file mode 100644 index 0000000..33895ab --- /dev/null +++ b/management-ui/README.md @@ -0,0 +1,5 @@ +# Vue 3 + TypeScript + Vite + +This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 ` + + diff --git a/management-ui/package-lock.json b/management-ui/package-lock.json new file mode 100644 index 0000000..fed64d4 --- /dev/null +++ b/management-ui/package-lock.json @@ -0,0 +1,2771 @@ +{ + "name": "management-ui", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "management-ui", + "version": "0.0.0", + "dependencies": { + "@tailwindcss/vite": "^4.1.11", + "@tanstack/vue-table": "^8.21.3", + "@vueuse/core": "^13.5.0", + "axios": "^1.11.0", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "lucide-vue-next": "^0.525.0", + "reka-ui": "^2.4.0", + "tailwind-merge": "^3.3.1", + "tailwindcss": "^4.1.11", + "tw-animate-css": "^1.3.6", + "uuid": "^11.1.0", + "vue": "^3.5.17", + "vue-router": "^4.5.1" + }, + "devDependencies": { + "@iconify-json/radix-icons": "^1.2.2", + "@iconify/vue": "^5.0.0", + "@types/node": "^24.1.0", + "@vitejs/plugin-vue": "^6.0.0", + "@vue/tsconfig": "^0.7.0", + "typescript": "~5.8.3", + "vite": "^7.0.4", + "vue-tsc": "^2.2.12" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.0.tgz", + "integrity": "sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.2", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.2.tgz", + "integrity": "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.8.tgz", + "integrity": "sha512-urAvrUedIqEiFR3FYSLTWQgLu5tb+m0qZw0NBEasUeo6wuqatkMDaRT+1uABiGXEu5vqgPd7FGE1BhsAIy9QVA==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.8.tgz", + "integrity": "sha512-RONsAvGCz5oWyePVnLdZY/HHwA++nxYWIX1atInlaW6SEkwq6XkP3+cb825EUcRs5Vss/lGh/2YxAb5xqc07Uw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.8.tgz", + "integrity": "sha512-OD3p7LYzWpLhZEyATcTSJ67qB5D+20vbtr6vHlHWSQYhKtzUYrETuWThmzFpZtFsBIxRvhO07+UgVA9m0i/O1w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.8.tgz", + "integrity": "sha512-yJAVPklM5+4+9dTeKwHOaA+LQkmrKFX96BM0A/2zQrbS6ENCmxc4OVoBs5dPkCCak2roAD+jKCdnmOqKszPkjA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.8.tgz", + "integrity": "sha512-Jw0mxgIaYX6R8ODrdkLLPwBqHTtYHJSmzzd+QeytSugzQ0Vg4c5rDky5VgkoowbZQahCbsv1rT1KW72MPIkevw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.8.tgz", + "integrity": "sha512-Vh2gLxxHnuoQ+GjPNvDSDRpoBCUzY4Pu0kBqMBDlK4fuWbKgGtmDIeEC081xi26PPjn+1tct+Bh8FjyLlw1Zlg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.8.tgz", + "integrity": "sha512-YPJ7hDQ9DnNe5vxOm6jaie9QsTwcKedPvizTVlqWG9GBSq+BuyWEDazlGaDTC5NGU4QJd666V0yqCBL2oWKPfA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.8.tgz", + "integrity": "sha512-MmaEXxQRdXNFsRN/KcIimLnSJrk2r5H8v+WVafRWz5xdSVmWLoITZQXcgehI2ZE6gioE6HirAEToM/RvFBeuhw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.8.tgz", + "integrity": "sha512-FuzEP9BixzZohl1kLf76KEVOsxtIBFwCaLupVuk4eFVnOZfU+Wsn+x5Ryam7nILV2pkq2TqQM9EZPsOBuMC+kg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.8.tgz", + "integrity": "sha512-WIgg00ARWv/uYLU7lsuDK00d/hHSfES5BzdWAdAig1ioV5kaFNrtK8EqGcUBJhYqotlUByUKz5Qo6u8tt7iD/w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.8.tgz", + "integrity": "sha512-A1D9YzRX1i+1AJZuFFUMP1E9fMaYY+GnSQil9Tlw05utlE86EKTUA7RjwHDkEitmLYiFsRd9HwKBPEftNdBfjg==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.8.tgz", + "integrity": "sha512-O7k1J/dwHkY1RMVvglFHl1HzutGEFFZ3kNiDMSOyUrB7WcoHGf96Sh+64nTRT26l3GMbCW01Ekh/ThKM5iI7hQ==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.8.tgz", + "integrity": "sha512-uv+dqfRazte3BzfMp8PAQXmdGHQt2oC/y2ovwpTteqrMx2lwaksiFZ/bdkXJC19ttTvNXBuWH53zy/aTj1FgGw==", + "cpu": [ + "mips64el" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.8.tgz", + "integrity": "sha512-GyG0KcMi1GBavP5JgAkkstMGyMholMDybAf8wF5A70CALlDM2p/f7YFE7H92eDeH/VBtFJA5MT4nRPDGg4JuzQ==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.8.tgz", + "integrity": "sha512-rAqDYFv3yzMrq7GIcen3XP7TUEG/4LK86LUPMIz6RT8A6pRIDn0sDcvjudVZBiiTcZCY9y2SgYX2lgK3AF+1eg==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.8.tgz", + "integrity": "sha512-Xutvh6VjlbcHpsIIbwY8GVRbwoviWT19tFhgdA7DlenLGC/mbc3lBoVb7jxj9Z+eyGqvcnSyIltYUrkKzWqSvg==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.8.tgz", + "integrity": "sha512-ASFQhgY4ElXh3nDcOMTkQero4b1lgubskNlhIfJrsH5OKZXDpUAKBlNS0Kx81jwOBp+HCeZqmoJuihTv57/jvQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.8.tgz", + "integrity": "sha512-d1KfruIeohqAi6SA+gENMuObDbEjn22olAR7egqnkCD9DGBG0wsEARotkLgXDu6c4ncgWTZJtN5vcgxzWRMzcw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.8.tgz", + "integrity": "sha512-nVDCkrvx2ua+XQNyfrujIG38+YGyuy2Ru9kKVNyh5jAys6n+l44tTtToqHjino2My8VAY6Lw9H7RI73XFi66Cg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.8.tgz", + "integrity": "sha512-j8HgrDuSJFAujkivSMSfPQSAa5Fxbvk4rgNAS5i3K+r8s1X0p1uOO2Hl2xNsGFppOeHOLAVgYwDVlmxhq5h+SQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.8.tgz", + "integrity": "sha512-1h8MUAwa0VhNCDp6Af0HToI2TJFAn1uqT9Al6DJVzdIBAd21m/G0Yfc77KDM3uF3T/YaOgQq3qTJHPbTOInaIQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.8.tgz", + "integrity": "sha512-r2nVa5SIK9tSWd0kJd9HCffnDHKchTGikb//9c7HX+r+wHYCpQrSgxhlY6KWV1nFo1l4KFbsMlHk+L6fekLsUg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.8.tgz", + "integrity": "sha512-zUlaP2S12YhQ2UzUfcCuMDHQFJyKABkAjvO5YSndMiIkMimPmxA+BYSBikWgsRpvyxuRnow4nS5NPnf9fpv41w==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.8.tgz", + "integrity": "sha512-YEGFFWESlPva8hGL+zvj2z/SaK+pH0SwOM0Nc/d+rVnW7GSTFlLBGzZkuSU9kFIGIo8q9X3ucpZhu8PDN5A2sQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.8.tgz", + "integrity": "sha512-hiGgGC6KZ5LZz58OL/+qVVoZiuZlUYlYHNAmczOm7bs2oE1XriPFi5ZHHrS8ACpV5EjySrnoCKmcbQMN+ojnHg==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.8.tgz", + "integrity": "sha512-cn3Yr7+OaaZq1c+2pe+8yxC8E144SReCQjN6/2ynubzYjvyqZjTXfQJpAcQpsdJq3My7XADANiYGHoFC69pLQw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@floating-ui/core": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.2.tgz", + "integrity": "sha512-wNB5ooIKHQc+Kui96jE/n69rHFWAVoxn5CAzL1Xdd8FG03cgY3MLO+GF9U3W737fYDSgPWA6MReKhBQBop6Pcw==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.2.tgz", + "integrity": "sha512-7cfaOQuCS27HD7DX+6ib2OrnW+b4ZBwDNnCcT0uTyidcmyWb03FnQqJybDBoCnpdxwBSfA94UAYlRCt7mV+TbA==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.7.2", + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz", + "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==", + "license": "MIT" + }, + "node_modules/@floating-ui/vue": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@floating-ui/vue/-/vue-1.1.7.tgz", + "integrity": "sha512-idmAtbAIigGXN2SI5gItiXYBYtNfDTP9yIiObxgu13dgtG7ARCHlNfnR29GxP4LI4o13oiwsJ8wVgghj1lNqcw==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.7.2", + "@floating-ui/utils": "^0.2.10", + "vue-demi": ">=0.13.0" + } + }, + "node_modules/@floating-ui/vue/node_modules/vue-demi": { + "version": "0.14.10", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz", + "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, + "node_modules/@iconify-json/radix-icons": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@iconify-json/radix-icons/-/radix-icons-1.2.2.tgz", + "integrity": "sha512-+PPmKWDP7pfJMcEc9Ty1zyo/zzq+9rfKW4EGb2HSZcPu1VUhothDLFzWvBqQNoFIOYCJ2nm0Vmf8kVyYhq9G0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@iconify/types": "*" + } + }, + "node_modules/@iconify/types": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@iconify/types/-/types-2.0.0.tgz", + "integrity": "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@iconify/vue": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@iconify/vue/-/vue-5.0.0.tgz", + "integrity": "sha512-C+KuEWIF5nSBrobFJhT//JS87OZ++QDORB6f2q2Wm6fl2mueSTpFBeBsveK0KW9hWiZ4mNiPjsh6Zs4jjdROSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@iconify/types": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/cyberalien" + }, + "peerDependencies": { + "vue": ">=3" + } + }, + "node_modules/@internationalized/date": { + "version": "3.8.2", + "resolved": "https://registry.npmjs.org/@internationalized/date/-/date-3.8.2.tgz", + "integrity": "sha512-/wENk7CbvLbkUvX1tu0mwq49CVkkWpkXubGel6birjRPyo6uQ4nQpnq5xZu823zRCwwn82zgHrvgF1vZyvmVgA==", + "license": "Apache-2.0", + "dependencies": { + "@swc/helpers": "^0.5.0" + } + }, + "node_modules/@internationalized/number": { + "version": "3.6.4", + "resolved": "https://registry.npmjs.org/@internationalized/number/-/number-3.6.4.tgz", + "integrity": "sha512-P+/h+RDaiX8EGt3shB9AYM1+QgkvHmJ5rKi4/59k4sg9g58k9rqsRW0WxRO7jCoHyvVbFRRFKmVTdFYdehrxHg==", + "license": "Apache-2.0", + "dependencies": { + "@swc/helpers": "^0.5.0" + } + }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.12", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.12.tgz", + "integrity": "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.4.tgz", + "integrity": "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.29", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.29.tgz", + "integrity": "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.19", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.19.tgz", + "integrity": "sha512-3FL3mnMbPu0muGOCaKAhhFEYmqv9eTfPSJRJmANrCwtgK8VuxpsZDGK+m0LYAGoyO8+0j5uRe4PeyPDK1yA/hA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.45.1.tgz", + "integrity": "sha512-NEySIFvMY0ZQO+utJkgoMiCAjMrGvnbDLHvcmlA33UXJpYBCvlBEbMMtV837uCkS+plG2umfhn0T5mMAxGrlRA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.45.1.tgz", + "integrity": "sha512-ujQ+sMXJkg4LRJaYreaVx7Z/VMgBBd89wGS4qMrdtfUFZ+TSY5Rs9asgjitLwzeIbhwdEhyj29zhst3L1lKsRQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.45.1.tgz", + "integrity": "sha512-FSncqHvqTm3lC6Y13xncsdOYfxGSLnP+73k815EfNmpewPs+EyM49haPS105Rh4aF5mJKywk9X0ogzLXZzN9lA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.45.1.tgz", + "integrity": "sha512-2/vVn/husP5XI7Fsf/RlhDaQJ7x9zjvC81anIVbr4b/f0xtSmXQTFcGIQ/B1cXIYM6h2nAhJkdMHTnD7OtQ9Og==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.45.1.tgz", + "integrity": "sha512-4g1kaDxQItZsrkVTdYQ0bxu4ZIQ32cotoQbmsAnW1jAE4XCMbcBPDirX5fyUzdhVCKgPcrwWuucI8yrVRBw2+g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.45.1.tgz", + "integrity": "sha512-L/6JsfiL74i3uK1Ti2ZFSNsp5NMiM4/kbbGEcOCps99aZx3g8SJMO1/9Y0n/qKlWZfn6sScf98lEOUe2mBvW9A==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.45.1.tgz", + "integrity": "sha512-RkdOTu2jK7brlu+ZwjMIZfdV2sSYHK2qR08FUWcIoqJC2eywHbXr0L8T/pONFwkGukQqERDheaGTeedG+rra6Q==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.45.1.tgz", + "integrity": "sha512-3kJ8pgfBt6CIIr1o+HQA7OZ9mp/zDk3ctekGl9qn/pRBgrRgfwiffaUmqioUGN9hv0OHv2gxmvdKOkARCtRb8Q==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.45.1.tgz", + "integrity": "sha512-k3dOKCfIVixWjG7OXTCOmDfJj3vbdhN0QYEqB+OuGArOChek22hn7Uy5A/gTDNAcCy5v2YcXRJ/Qcnm4/ma1xw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.45.1.tgz", + "integrity": "sha512-PmI1vxQetnM58ZmDFl9/Uk2lpBBby6B6rF4muJc65uZbxCs0EA7hhKCk2PKlmZKuyVSHAyIw3+/SiuMLxKxWog==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loongarch64-gnu": { + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.45.1.tgz", + "integrity": "sha512-9UmI0VzGmNJ28ibHW2GpE2nF0PBQqsyiS4kcJ5vK+wuwGnV5RlqdczVocDSUfGX/Na7/XINRVoUgJyFIgipoRg==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.45.1.tgz", + "integrity": "sha512-7nR2KY8oEOUTD3pBAxIBBbZr0U7U+R9HDTPNy+5nVVHDXI4ikYniH1oxQz9VoB5PbBU1CZuDGHkLJkd3zLMWsg==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.45.1.tgz", + "integrity": "sha512-nlcl3jgUultKROfZijKjRQLUu9Ma0PeNv/VFHkZiKbXTBQXhpytS8CIj5/NfBeECZtY2FJQubm6ltIxm/ftxpw==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.45.1.tgz", + "integrity": "sha512-HJV65KLS51rW0VY6rvZkiieiBnurSzpzore1bMKAhunQiECPuxsROvyeaot/tcK3A3aGnI+qTHqisrpSgQrpgA==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.45.1.tgz", + "integrity": "sha512-NITBOCv3Qqc6hhwFt7jLV78VEO/il4YcBzoMGGNxznLgRQf43VQDae0aAzKiBeEPIxnDrACiMgbqjuihx08OOw==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.45.1.tgz", + "integrity": "sha512-+E/lYl6qu1zqgPEnTrs4WysQtvc/Sh4fC2nByfFExqgYrqkKWp1tWIbe+ELhixnenSpBbLXNi6vbEEJ8M7fiHw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.45.1.tgz", + "integrity": "sha512-a6WIAp89p3kpNoYStITT9RbTbTnqarU7D8N8F2CV+4Cl9fwCOZraLVuVFvlpsW0SbIiYtEnhCZBPLoNdRkjQFw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.45.1.tgz", + "integrity": "sha512-T5Bi/NS3fQiJeYdGvRpTAP5P02kqSOpqiopwhj0uaXB6nzs5JVi2XMJb18JUSKhCOX8+UE1UKQufyD6Or48dJg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.45.1.tgz", + "integrity": "sha512-lxV2Pako3ujjuUe9jiU3/s7KSrDfH6IgTSQOnDWr9aJ92YsFd7EurmClK0ly/t8dzMkDtd04g60WX6yl0sGfdw==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.45.1.tgz", + "integrity": "sha512-M/fKi4sasCdM8i0aWJjCSFm2qEnYRR8AMLG2kxp6wD13+tMGA4Z1tVAuHkNRjud5SW2EM3naLuK35w9twvf6aA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@swc/helpers": { + "version": "0.5.17", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.17.tgz", + "integrity": "sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@tailwindcss/node": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.11.tgz", + "integrity": "sha512-yzhzuGRmv5QyU9qLNg4GTlYI6STedBWRE7NjxP45CsFYYq9taI0zJXZBMqIC/c8fViNLhmrbpSFS57EoxUmD6Q==", + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.3.0", + "enhanced-resolve": "^5.18.1", + "jiti": "^2.4.2", + "lightningcss": "1.30.1", + "magic-string": "^0.30.17", + "source-map-js": "^1.2.1", + "tailwindcss": "4.1.11" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.11.tgz", + "integrity": "sha512-Q69XzrtAhuyfHo+5/HMgr1lAiPP/G40OMFAnws7xcFEYqcypZmdW8eGXaOUIeOl1dzPJBPENXgbjsOyhg2nkrg==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.4", + "tar": "^7.4.3" + }, + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.1.11", + "@tailwindcss/oxide-darwin-arm64": "4.1.11", + "@tailwindcss/oxide-darwin-x64": "4.1.11", + "@tailwindcss/oxide-freebsd-x64": "4.1.11", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.11", + "@tailwindcss/oxide-linux-arm64-gnu": "4.1.11", + "@tailwindcss/oxide-linux-arm64-musl": "4.1.11", + "@tailwindcss/oxide-linux-x64-gnu": "4.1.11", + "@tailwindcss/oxide-linux-x64-musl": "4.1.11", + "@tailwindcss/oxide-wasm32-wasi": "4.1.11", + "@tailwindcss/oxide-win32-arm64-msvc": "4.1.11", + "@tailwindcss/oxide-win32-x64-msvc": "4.1.11" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.11.tgz", + "integrity": "sha512-3IfFuATVRUMZZprEIx9OGDjG3Ou3jG4xQzNTvjDoKmU9JdmoCohQJ83MYd0GPnQIu89YoJqvMM0G3uqLRFtetg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.11.tgz", + "integrity": "sha512-ESgStEOEsyg8J5YcMb1xl8WFOXfeBmrhAwGsFxxB2CxY9evy63+AtpbDLAyRkJnxLy2WsD1qF13E97uQyP1lfQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.11.tgz", + "integrity": "sha512-EgnK8kRchgmgzG6jE10UQNaH9Mwi2n+yw1jWmof9Vyg2lpKNX2ioe7CJdf9M5f8V9uaQxInenZkOxnTVL3fhAw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.11.tgz", + "integrity": "sha512-xdqKtbpHs7pQhIKmqVpxStnY1skuNh4CtbcyOHeX1YBE0hArj2romsFGb6yUmzkq/6M24nkxDqU8GYrKrz+UcA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.11.tgz", + "integrity": "sha512-ryHQK2eyDYYMwB5wZL46uoxz2zzDZsFBwfjssgB7pzytAeCCa6glsiJGjhTEddq/4OsIjsLNMAiMlHNYnkEEeg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.11.tgz", + "integrity": "sha512-mYwqheq4BXF83j/w75ewkPJmPZIqqP1nhoghS9D57CLjsh3Nfq0m4ftTotRYtGnZd3eCztgbSPJ9QhfC91gDZQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.11.tgz", + "integrity": "sha512-m/NVRFNGlEHJrNVk3O6I9ggVuNjXHIPoD6bqay/pubtYC9QIdAMpS+cswZQPBLvVvEF6GtSNONbDkZrjWZXYNQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.11.tgz", + "integrity": "sha512-YW6sblI7xukSD2TdbbaeQVDysIm/UPJtObHJHKxDEcW2exAtY47j52f8jZXkqE1krdnkhCMGqP3dbniu1Te2Fg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.11.tgz", + "integrity": "sha512-e3C/RRhGunWYNC3aSF7exsQkdXzQ/M+aYuZHKnw4U7KQwTJotnWsGOIVih0s2qQzmEzOFIJ3+xt7iq67K/p56Q==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.11.tgz", + "integrity": "sha512-Xo1+/GU0JEN/C/dvcammKHzeM6NqKovG+6921MR6oadee5XPBaKOumrJCXvopJ/Qb5TH7LX/UAywbqrP4lax0g==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.4.3", + "@emnapi/runtime": "^1.4.3", + "@emnapi/wasi-threads": "^1.0.2", + "@napi-rs/wasm-runtime": "^0.2.11", + "@tybys/wasm-util": "^0.9.0", + "tslib": "^2.8.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.11.tgz", + "integrity": "sha512-UgKYx5PwEKrac3GPNPf6HVMNhUIGuUh4wlDFR2jYYdkX6pL/rn73zTq/4pzUm8fOjAn5L8zDeHp9iXmUGOXZ+w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.11.tgz", + "integrity": "sha512-YfHoggn1j0LK7wR82TOucWc5LDCguHnoS879idHekmmiR7g9HUtMw9MI0NHatS28u/Xlkfi9w5RJWgz2Dl+5Qg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/vite": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.1.11.tgz", + "integrity": "sha512-RHYhrR3hku0MJFRV+fN2gNbDNEh3dwKvY8XJvTxCSXeMOsCRSr+uKvDWQcbizrHgjML6ZmTE5OwMrl5wKcujCw==", + "license": "MIT", + "dependencies": { + "@tailwindcss/node": "4.1.11", + "@tailwindcss/oxide": "4.1.11", + "tailwindcss": "4.1.11" + }, + "peerDependencies": { + "vite": "^5.2.0 || ^6 || ^7" + } + }, + "node_modules/@tanstack/table-core": { + "version": "8.21.3", + "resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.21.3.tgz", + "integrity": "sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/virtual-core": { + "version": "3.13.12", + "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.13.12.tgz", + "integrity": "sha512-1YBOJfRHV4sXUmWsFSf5rQor4Ss82G8dQWLRbnk3GA4jeP8hQt1hxXh0tmflpC0dz3VgEv/1+qwPyLeWkQuPFA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/vue-table": { + "version": "8.21.3", + "resolved": "https://registry.npmjs.org/@tanstack/vue-table/-/vue-table-8.21.3.tgz", + "integrity": "sha512-rusRyd77c5tDPloPskctMyPLFEQUeBzxdQ+2Eow4F7gDPlPOB1UnnhzfpdvqZ8ZyX2rRNGmqNnQWm87OI2OQPw==", + "license": "MIT", + "dependencies": { + "@tanstack/table-core": "8.21.3" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "vue": ">=3.2" + } + }, + "node_modules/@tanstack/vue-virtual": { + "version": "3.13.12", + "resolved": "https://registry.npmjs.org/@tanstack/vue-virtual/-/vue-virtual-3.13.12.tgz", + "integrity": "sha512-vhF7kEU9EXWXh+HdAwKJ2m3xaOnTTmgcdXcF2pim8g4GvI7eRrk2YRuV5nUlZnd/NbCIX4/Ja2OZu5EjJL06Ww==", + "license": "MIT", + "dependencies": { + "@tanstack/virtual-core": "3.13.12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "vue": "^2.7.0 || ^3.0.0" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.1.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.1.0.tgz", + "integrity": "sha512-ut5FthK5moxFKH2T1CUOC6ctR67rQRvvHdFLCD2Ql6KXmMuCrjsSsRI9UsLCm9M18BMwClv4pn327UvB7eeO1w==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.8.0" + } + }, + "node_modules/@types/web-bluetooth": { + "version": "0.0.21", + "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz", + "integrity": "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==", + "license": "MIT" + }, + "node_modules/@vitejs/plugin-vue": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.0.tgz", + "integrity": "sha512-iAliE72WsdhjzTOp2DtvKThq1VBC4REhwRcaA+zPAAph6I+OQhUXv+Xu2KS7ElxYtb7Zc/3R30Hwv1DxEo7NXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "1.0.0-beta.19" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0", + "vue": "^3.2.25" + } + }, + "node_modules/@volar/language-core": { + "version": "2.4.15", + "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.15.tgz", + "integrity": "sha512-3VHw+QZU0ZG9IuQmzT68IyN4hZNd9GchGPhbD9+pa8CVv7rnoOZwo7T8weIbrRmihqy3ATpdfXFnqRrfPVK6CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/source-map": "2.4.15" + } + }, + "node_modules/@volar/source-map": { + "version": "2.4.15", + "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.15.tgz", + "integrity": "sha512-CPbMWlUN6hVZJYGcU/GSoHu4EnCHiLaXI9n8c9la6RaI9W5JHX+NqG+GSQcB0JdC2FIBLdZJwGsfKyBB71VlTg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@volar/typescript": { + "version": "2.4.15", + "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.15.tgz", + "integrity": "sha512-2aZ8i0cqPGjXb4BhkMsPYDkkuc2ZQ6yOpqwAuNwUoncELqoy5fRgOQtLR9gB0g902iS0NAkvpIzs27geVyVdPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/language-core": "2.4.15", + "path-browserify": "^1.0.1", + "vscode-uri": "^3.0.8" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.18", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.18.tgz", + "integrity": "sha512-3slwjQrrV1TO8MoXgy3aynDQ7lslj5UqDxuHnrzHtpON5CBinhWjJETciPngpin/T3OuW3tXUf86tEurusnztw==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.0", + "@vue/shared": "3.5.18", + "entities": "^4.5.0", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.18", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.18.tgz", + "integrity": "sha512-RMbU6NTU70++B1JyVJbNbeFkK+A+Q7y9XKE2EM4NLGm2WFR8x9MbAtWxPPLdm0wUkuZv9trpwfSlL6tjdIa1+A==", + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.18", + "@vue/shared": "3.5.18" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.18", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.18.tgz", + "integrity": "sha512-5aBjvGqsWs+MoxswZPoTB9nSDb3dhd1x30xrrltKujlCxo48j8HGDNj3QPhF4VIS0VQDUrA1xUfp2hEa+FNyXA==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.0", + "@vue/compiler-core": "3.5.18", + "@vue/compiler-dom": "3.5.18", + "@vue/compiler-ssr": "3.5.18", + "@vue/shared": "3.5.18", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.17", + "postcss": "^8.5.6", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.18", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.18.tgz", + "integrity": "sha512-xM16Ak7rSWHkM3m22NlmcdIM+K4BMyFARAfV9hYFl+SFuRzrZ3uGMNW05kA5pmeMa0X9X963Kgou7ufdbpOP9g==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.18", + "@vue/shared": "3.5.18" + } + }, + "node_modules/@vue/compiler-vue2": { + "version": "2.7.16", + "resolved": "https://registry.npmjs.org/@vue/compiler-vue2/-/compiler-vue2-2.7.16.tgz", + "integrity": "sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A==", + "dev": true, + "license": "MIT", + "dependencies": { + "de-indent": "^1.0.2", + "he": "^1.2.0" + } + }, + "node_modules/@vue/devtools-api": { + "version": "6.6.4", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz", + "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==", + "license": "MIT" + }, + "node_modules/@vue/language-core": { + "version": "2.2.12", + "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-2.2.12.tgz", + "integrity": "sha512-IsGljWbKGU1MZpBPN+BvPAdr55YPkj2nB/TBNGNC32Vy2qLG25DYu/NBN2vNtZqdRbTRjaoYrahLrToim2NanA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/language-core": "2.4.15", + "@vue/compiler-dom": "^3.5.0", + "@vue/compiler-vue2": "^2.7.16", + "@vue/shared": "^3.5.0", + "alien-signals": "^1.0.3", + "minimatch": "^9.0.3", + "muggle-string": "^0.4.1", + "path-browserify": "^1.0.1" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@vue/reactivity": { + "version": "3.5.18", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.18.tgz", + "integrity": "sha512-x0vPO5Imw+3sChLM5Y+B6G1zPjwdOri9e8V21NnTnlEvkxatHEH5B5KEAJcjuzQ7BsjGrKtfzuQ5eQwXh8HXBg==", + "license": "MIT", + "dependencies": { + "@vue/shared": "3.5.18" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.18", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.18.tgz", + "integrity": "sha512-DUpHa1HpeOQEt6+3nheUfqVXRog2kivkXHUhoqJiKR33SO4x+a5uNOMkV487WPerQkL0vUuRvq/7JhRgLW3S+w==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.18", + "@vue/shared": "3.5.18" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.18", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.18.tgz", + "integrity": "sha512-YwDj71iV05j4RnzZnZtGaXwPoUWeRsqinblgVJwR8XTXYZ9D5PbahHQgsbmzUvCWNF6x7siQ89HgnX5eWkr3mw==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.18", + "@vue/runtime-core": "3.5.18", + "@vue/shared": "3.5.18", + "csstype": "^3.1.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.18", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.18.tgz", + "integrity": "sha512-PvIHLUoWgSbDG7zLHqSqaCoZvHi6NNmfVFOqO+OnwvqMz/tqQr3FuGWS8ufluNddk7ZLBJYMrjcw1c6XzR12mA==", + "license": "MIT", + "dependencies": { + "@vue/compiler-ssr": "3.5.18", + "@vue/shared": "3.5.18" + }, + "peerDependencies": { + "vue": "3.5.18" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.18", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.18.tgz", + "integrity": "sha512-cZy8Dq+uuIXbxCZpuLd2GJdeSO/lIzIspC2WtkqIpje5QyFbvLaI5wZtdUjLHjGZrlVX6GilejatWwVYYRc8tA==", + "license": "MIT" + }, + "node_modules/@vue/tsconfig": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@vue/tsconfig/-/tsconfig-0.7.0.tgz", + "integrity": "sha512-ku2uNz5MaZ9IerPPUyOHzyjhXoX2kVJaVf7hL315DC17vS6IiZRmmCPfggNbU16QTvM80+uYYy3eYJB59WCtvg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "typescript": "5.x", + "vue": "^3.4.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + }, + "vue": { + "optional": true + } + } + }, + "node_modules/@vueuse/core": { + "version": "13.5.0", + "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-13.5.0.tgz", + "integrity": "sha512-wV7z0eUpifKmvmN78UBZX8T7lMW53Nrk6JP5+6hbzrB9+cJ3jr//hUlhl9TZO/03bUkMK6gGkQpqOPWoabr72g==", + "license": "MIT", + "dependencies": { + "@types/web-bluetooth": "^0.0.21", + "@vueuse/metadata": "13.5.0", + "@vueuse/shared": "13.5.0" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "vue": "^3.5.0" + } + }, + "node_modules/@vueuse/metadata": { + "version": "13.5.0", + "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-13.5.0.tgz", + "integrity": "sha512-euhItU3b0SqXxSy8u1XHxUCdQ8M++bsRs+TYhOLDU/OykS7KvJnyIFfep0XM5WjIFry9uAPlVSjmVHiqeshmkw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/shared": { + "version": "13.5.0", + "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-13.5.0.tgz", + "integrity": "sha512-K7GrQIxJ/ANtucxIXbQlUHdB0TPA8c+q5i+zbrjxuhJCnJ9GtBg75sBSnvmLSxHKPg2Yo8w62PWksl9kwH0Q8g==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "vue": "^3.5.0" + } + }, + "node_modules/alien-signals": { + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-1.0.13.tgz", + "integrity": "sha512-OGj9yyTnJEttvzhTUWuscOvtqxq5vrhF7vL9oS0xJ2mK0ItPYP1/y+vCFebfxoEyAz0++1AIwJ5CMr+Fk3nDmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/aria-hidden": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz", + "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "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.11.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.11.0.tgz", + "integrity": "sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" + } + }, + "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": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/class-variance-authority": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", + "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==", + "license": "Apache-2.0", + "dependencies": { + "clsx": "^2.1.1" + }, + "funding": { + "url": "https://polar.sh/cva" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "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/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "license": "MIT" + }, + "node_modules/de-indent": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz", + "integrity": "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==", + "dev": true, + "license": "MIT" + }, + "node_modules/defu": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz", + "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==", + "license": "MIT" + }, + "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/detect-libc": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", + "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.18.2", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.2.tgz", + "integrity": "sha512-6Jw4sE1maoRJo3q8MsSIn2onJFbLTOjY9hlx4DZXmOKvLRd1Ok2kXmAGXaafL2+ijsJZ1ClYbl/pmqr9+k4iUQ==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.8.tgz", + "integrity": "sha512-vVC0USHGtMi8+R4Kz8rt6JhEWLxsv9Rnu/lGYbPR8u47B+DCBksq9JarW0zOO7bs37hyOK1l2/oqtbciutL5+Q==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.8", + "@esbuild/android-arm": "0.25.8", + "@esbuild/android-arm64": "0.25.8", + "@esbuild/android-x64": "0.25.8", + "@esbuild/darwin-arm64": "0.25.8", + "@esbuild/darwin-x64": "0.25.8", + "@esbuild/freebsd-arm64": "0.25.8", + "@esbuild/freebsd-x64": "0.25.8", + "@esbuild/linux-arm": "0.25.8", + "@esbuild/linux-arm64": "0.25.8", + "@esbuild/linux-ia32": "0.25.8", + "@esbuild/linux-loong64": "0.25.8", + "@esbuild/linux-mips64el": "0.25.8", + "@esbuild/linux-ppc64": "0.25.8", + "@esbuild/linux-riscv64": "0.25.8", + "@esbuild/linux-s390x": "0.25.8", + "@esbuild/linux-x64": "0.25.8", + "@esbuild/netbsd-arm64": "0.25.8", + "@esbuild/netbsd-x64": "0.25.8", + "@esbuild/openbsd-arm64": "0.25.8", + "@esbuild/openbsd-x64": "0.25.8", + "@esbuild/openharmony-arm64": "0.25.8", + "@esbuild/sunos-x64": "0.25.8", + "@esbuild/win32-arm64": "0.25.8", + "@esbuild/win32-ia32": "0.25.8", + "@esbuild/win32-x64": "0.25.8" + } + }, + "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==", + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.4.6", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz", + "integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==", + "license": "MIT", + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "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.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "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==", + "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==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "license": "MIT", + "bin": { + "he": "bin/he" + } + }, + "node_modules/jiti": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.5.1.tgz", + "integrity": "sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w==", + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/lightningcss": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.1.tgz", + "integrity": "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==", + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-darwin-arm64": "1.30.1", + "lightningcss-darwin-x64": "1.30.1", + "lightningcss-freebsd-x64": "1.30.1", + "lightningcss-linux-arm-gnueabihf": "1.30.1", + "lightningcss-linux-arm64-gnu": "1.30.1", + "lightningcss-linux-arm64-musl": "1.30.1", + "lightningcss-linux-x64-gnu": "1.30.1", + "lightningcss-linux-x64-musl": "1.30.1", + "lightningcss-win32-arm64-msvc": "1.30.1", + "lightningcss-win32-x64-msvc": "1.30.1" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.1.tgz", + "integrity": "sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.1.tgz", + "integrity": "sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.1.tgz", + "integrity": "sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.1.tgz", + "integrity": "sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q==", + "cpu": [ + "arm" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.1.tgz", + "integrity": "sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.1.tgz", + "integrity": "sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.1.tgz", + "integrity": "sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.1.tgz", + "integrity": "sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.1.tgz", + "integrity": "sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.1.tgz", + "integrity": "sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lucide-vue-next": { + "version": "0.525.0", + "resolved": "https://registry.npmjs.org/lucide-vue-next/-/lucide-vue-next-0.525.0.tgz", + "integrity": "sha512-Xf8+x8B2DrnGDV/rxylS+KBp2FIe6ljwDn2JsGTZZvXIfhmm/q+nv8RuGO1OyoMjOVkkz7CqtUqJfwtFPRbB2w==", + "license": "ISC", + "peerDependencies": { + "vue": ">=3.0.1" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "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": "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/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minizlib": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.0.2.tgz", + "integrity": "sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==", + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/mkdirp": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", + "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", + "license": "MIT", + "bin": { + "mkdirp": "dist/cjs/src/bin.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/muggle-string": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.4.1.tgz", + "integrity": "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "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/ohash": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz", + "integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==", + "license": "MIT" + }, + "node_modules/path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "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==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "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.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "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/reka-ui": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/reka-ui/-/reka-ui-2.4.0.tgz", + "integrity": "sha512-5WHLEquWI5W67NnjH9F+RhpzRAcwjAEQHtHZMa5wYdhClcYDr59q0RAAcymcnfndFqv0MiBxyFfzbGSRyIZG5g==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.6.13", + "@floating-ui/vue": "^1.1.6", + "@internationalized/date": "^3.5.0", + "@internationalized/number": "^3.5.0", + "@tanstack/vue-virtual": "^3.12.0", + "@vueuse/core": "^12.5.0", + "@vueuse/shared": "^12.5.0", + "aria-hidden": "^1.2.4", + "defu": "^6.1.4", + "ohash": "^2.0.11" + }, + "peerDependencies": { + "vue": ">= 3.2.0" + } + }, + "node_modules/reka-ui/node_modules/@vueuse/core": { + "version": "12.8.2", + "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-12.8.2.tgz", + "integrity": "sha512-HbvCmZdzAu3VGi/pWYm5Ut+Kd9mn1ZHnn4L5G8kOQTPs/IwIAmJoBrmYk2ckLArgMXZj0AW3n5CAejLUO+PhdQ==", + "license": "MIT", + "dependencies": { + "@types/web-bluetooth": "^0.0.21", + "@vueuse/metadata": "12.8.2", + "@vueuse/shared": "12.8.2", + "vue": "^3.5.13" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/reka-ui/node_modules/@vueuse/metadata": { + "version": "12.8.2", + "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-12.8.2.tgz", + "integrity": "sha512-rAyLGEuoBJ/Il5AmFHiziCPdQzRt88VxR+Y/A/QhJ1EWtWqPBBAxTAFaSkviwEuOEZNtW8pvkPgoCZQ+HxqW1A==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/reka-ui/node_modules/@vueuse/shared": { + "version": "12.8.2", + "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-12.8.2.tgz", + "integrity": "sha512-dznP38YzxZoNloI0qpEfpkms8knDtaoQ6Y/sfS0L7Yki4zh40LFHEhur0odJC6xTHG5dxWVPiUWBXn+wCG2s5w==", + "license": "MIT", + "dependencies": { + "vue": "^3.5.13" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/rollup": { + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.45.1.tgz", + "integrity": "sha512-4iya7Jb76fVpQyLoiVpzUrsjQ12r3dM7fIVz+4NwoYvZOShknRmiv+iu9CClZml5ZLGb0XMcYLutK6w9tgxHDw==", + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.45.1", + "@rollup/rollup-android-arm64": "4.45.1", + "@rollup/rollup-darwin-arm64": "4.45.1", + "@rollup/rollup-darwin-x64": "4.45.1", + "@rollup/rollup-freebsd-arm64": "4.45.1", + "@rollup/rollup-freebsd-x64": "4.45.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.45.1", + "@rollup/rollup-linux-arm-musleabihf": "4.45.1", + "@rollup/rollup-linux-arm64-gnu": "4.45.1", + "@rollup/rollup-linux-arm64-musl": "4.45.1", + "@rollup/rollup-linux-loongarch64-gnu": "4.45.1", + "@rollup/rollup-linux-powerpc64le-gnu": "4.45.1", + "@rollup/rollup-linux-riscv64-gnu": "4.45.1", + "@rollup/rollup-linux-riscv64-musl": "4.45.1", + "@rollup/rollup-linux-s390x-gnu": "4.45.1", + "@rollup/rollup-linux-x64-gnu": "4.45.1", + "@rollup/rollup-linux-x64-musl": "4.45.1", + "@rollup/rollup-win32-arm64-msvc": "4.45.1", + "@rollup/rollup-win32-ia32-msvc": "4.45.1", + "@rollup/rollup-win32-x64-msvc": "4.45.1", + "fsevents": "~2.3.2" + } + }, + "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==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tailwind-merge": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.3.1.tgz", + "integrity": "sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, + "node_modules/tailwindcss": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.11.tgz", + "integrity": "sha512-2E9TBm6MDD/xKYe+dvJZAmg3yxIEDNRc0jwlNyDg/4Fil2QcSLjFKGVff0lAf1jjeaArlG/M75Ey/EYr/OJtBA==", + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.2.tgz", + "integrity": "sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/tar": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz", + "integrity": "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==", + "license": "ISC", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.0.1", + "mkdirp": "^3.0.1", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.14", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz", + "integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==", + "license": "MIT", + "dependencies": { + "fdir": "^6.4.4", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/tw-animate-css": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/tw-animate-css/-/tw-animate-css-1.3.6.tgz", + "integrity": "sha512-9dy0R9UsYEGmgf26L8UcHiLmSFTHa9+D7+dAt/G/sF5dCnPePZbfgDYinc7/UzAM7g/baVrmS6m9yEpU46d+LA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Wombosvideo" + } + }, + "node_modules/typescript": { + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz", + "integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/uuid": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", + "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/esm/bin/uuid" + } + }, + "node_modules/vite": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.0.6.tgz", + "integrity": "sha512-MHFiOENNBd+Bd9uvc8GEsIzdkn1JxMmEeYX35tI3fv0sJBUTfW5tQsoaOwuY4KhBI09A3dUJ/DXf2yxPVPUceg==", + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.6", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.40.0", + "tinyglobby": "^0.2.14" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vscode-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz", + "integrity": "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/vue": { + "version": "3.5.18", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.18.tgz", + "integrity": "sha512-7W4Y4ZbMiQ3SEo+m9lnoNpV9xG7QVMLa+/0RFwwiAVkeYoyGXqWE85jabU4pllJNUzqfLShJ5YLptewhCWUgNA==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.18", + "@vue/compiler-sfc": "3.5.18", + "@vue/runtime-dom": "3.5.18", + "@vue/server-renderer": "3.5.18", + "@vue/shared": "3.5.18" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/vue-router": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.5.1.tgz", + "integrity": "sha512-ogAF3P97NPm8fJsE4by9dwSYtDwXIY1nFY9T6DyQnGHd1E2Da94w9JIolpe42LJGIl0DwOHBi8TcRPlPGwbTtw==", + "license": "MIT", + "dependencies": { + "@vue/devtools-api": "^6.6.4" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "vue": "^3.2.0" + } + }, + "node_modules/vue-tsc": { + "version": "2.2.12", + "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-2.2.12.tgz", + "integrity": "sha512-P7OP77b2h/Pmk+lZdJ0YWs+5tJ6J2+uOQPo7tlBnY44QqQSPYvS0qVT4wqDJgwrZaLe47etJLLQRFia71GYITw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/typescript": "2.4.15", + "@vue/language-core": "2.2.12" + }, + "bin": { + "vue-tsc": "bin/vue-tsc.js" + }, + "peerDependencies": { + "typescript": ">=5.0.0" + } + }, + "node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + } + } +} diff --git a/management-ui/package.json b/management-ui/package.json new file mode 100644 index 0000000..6c5912e --- /dev/null +++ b/management-ui/package.json @@ -0,0 +1,37 @@ +{ + "name": "management-ui", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vue-tsc -b && vite build", + "preview": "vite preview" + }, + "dependencies": { + "@tailwindcss/vite": "^4.1.11", + "@tanstack/vue-table": "^8.21.3", + "@vueuse/core": "^13.5.0", + "axios": "^1.11.0", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "lucide-vue-next": "^0.525.0", + "reka-ui": "^2.4.0", + "tailwind-merge": "^3.3.1", + "tailwindcss": "^4.1.11", + "tw-animate-css": "^1.3.6", + "uuid": "^11.1.0", + "vue": "^3.5.17", + "vue-router": "^4.5.1" + }, + "devDependencies": { + "@iconify-json/radix-icons": "^1.2.2", + "@iconify/vue": "^5.0.0", + "@types/node": "^24.1.0", + "@vitejs/plugin-vue": "^6.0.0", + "@vue/tsconfig": "^0.7.0", + "typescript": "~5.8.3", + "vite": "^7.0.4", + "vue-tsc": "^2.2.12" + } +} diff --git a/management-ui/public/vite.svg b/management-ui/public/vite.svg new file mode 100644 index 0000000..e7b8dfb --- /dev/null +++ b/management-ui/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/management-ui/src/App.vue b/management-ui/src/App.vue new file mode 100644 index 0000000..950cb89 --- /dev/null +++ b/management-ui/src/App.vue @@ -0,0 +1,7 @@ + + + + diff --git a/management-ui/src/assets/vue.svg b/management-ui/src/assets/vue.svg new file mode 100644 index 0000000..770e9d3 --- /dev/null +++ b/management-ui/src/assets/vue.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/management-ui/src/components/HelloWorld.vue b/management-ui/src/components/HelloWorld.vue new file mode 100644 index 0000000..b58e52b --- /dev/null +++ b/management-ui/src/components/HelloWorld.vue @@ -0,0 +1,41 @@ + + + + + diff --git a/management-ui/src/components/ui/alert-dialog/AlertDialog.vue b/management-ui/src/components/ui/alert-dialog/AlertDialog.vue new file mode 100644 index 0000000..f4d8c9a --- /dev/null +++ b/management-ui/src/components/ui/alert-dialog/AlertDialog.vue @@ -0,0 +1,14 @@ + + + diff --git a/management-ui/src/components/ui/alert-dialog/AlertDialogAction.vue b/management-ui/src/components/ui/alert-dialog/AlertDialogAction.vue new file mode 100644 index 0000000..a5586f2 --- /dev/null +++ b/management-ui/src/components/ui/alert-dialog/AlertDialogAction.vue @@ -0,0 +1,17 @@ + + + diff --git a/management-ui/src/components/ui/alert-dialog/AlertDialogCancel.vue b/management-ui/src/components/ui/alert-dialog/AlertDialogCancel.vue new file mode 100644 index 0000000..853968f --- /dev/null +++ b/management-ui/src/components/ui/alert-dialog/AlertDialogCancel.vue @@ -0,0 +1,24 @@ + + + diff --git a/management-ui/src/components/ui/alert-dialog/AlertDialogContent.vue b/management-ui/src/components/ui/alert-dialog/AlertDialogContent.vue new file mode 100644 index 0000000..c049348 --- /dev/null +++ b/management-ui/src/components/ui/alert-dialog/AlertDialogContent.vue @@ -0,0 +1,41 @@ + + + diff --git a/management-ui/src/components/ui/alert-dialog/AlertDialogDescription.vue b/management-ui/src/components/ui/alert-dialog/AlertDialogDescription.vue new file mode 100644 index 0000000..d04253a --- /dev/null +++ b/management-ui/src/components/ui/alert-dialog/AlertDialogDescription.vue @@ -0,0 +1,23 @@ + + + diff --git a/management-ui/src/components/ui/alert-dialog/AlertDialogFooter.vue b/management-ui/src/components/ui/alert-dialog/AlertDialogFooter.vue new file mode 100644 index 0000000..1b534ad --- /dev/null +++ b/management-ui/src/components/ui/alert-dialog/AlertDialogFooter.vue @@ -0,0 +1,22 @@ + + + diff --git a/management-ui/src/components/ui/alert-dialog/AlertDialogHeader.vue b/management-ui/src/components/ui/alert-dialog/AlertDialogHeader.vue new file mode 100644 index 0000000..d57edb9 --- /dev/null +++ b/management-ui/src/components/ui/alert-dialog/AlertDialogHeader.vue @@ -0,0 +1,17 @@ + + + diff --git a/management-ui/src/components/ui/alert-dialog/AlertDialogTitle.vue b/management-ui/src/components/ui/alert-dialog/AlertDialogTitle.vue new file mode 100644 index 0000000..240ea3f --- /dev/null +++ b/management-ui/src/components/ui/alert-dialog/AlertDialogTitle.vue @@ -0,0 +1,20 @@ + + + diff --git a/management-ui/src/components/ui/alert-dialog/AlertDialogTrigger.vue b/management-ui/src/components/ui/alert-dialog/AlertDialogTrigger.vue new file mode 100644 index 0000000..f7822d8 --- /dev/null +++ b/management-ui/src/components/ui/alert-dialog/AlertDialogTrigger.vue @@ -0,0 +1,11 @@ + + + diff --git a/management-ui/src/components/ui/alert-dialog/index.ts b/management-ui/src/components/ui/alert-dialog/index.ts new file mode 100644 index 0000000..0e1a4a8 --- /dev/null +++ b/management-ui/src/components/ui/alert-dialog/index.ts @@ -0,0 +1,9 @@ +export { default as AlertDialog } from './AlertDialog.vue' +export { default as AlertDialogAction } from './AlertDialogAction.vue' +export { default as AlertDialogCancel } from './AlertDialogCancel.vue' +export { default as AlertDialogContent } from './AlertDialogContent.vue' +export { default as AlertDialogDescription } from './AlertDialogDescription.vue' +export { default as AlertDialogFooter } from './AlertDialogFooter.vue' +export { default as AlertDialogHeader } from './AlertDialogHeader.vue' +export { default as AlertDialogTitle } from './AlertDialogTitle.vue' +export { default as AlertDialogTrigger } from './AlertDialogTrigger.vue' diff --git a/management-ui/src/components/ui/button/Button.vue b/management-ui/src/components/ui/button/Button.vue new file mode 100644 index 0000000..6498db9 --- /dev/null +++ b/management-ui/src/components/ui/button/Button.vue @@ -0,0 +1,27 @@ + + + diff --git a/management-ui/src/components/ui/button/index.ts b/management-ui/src/components/ui/button/index.ts new file mode 100644 index 0000000..23edd68 --- /dev/null +++ b/management-ui/src/components/ui/button/index.ts @@ -0,0 +1,36 @@ +import { cva, type VariantProps } from 'class-variance-authority' + +export { default as Button } from './Button.vue' + +export const buttonVariants = cva( + 'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*=\'size-\'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive', + { + variants: { + variant: { + default: + 'bg-primary text-primary-foreground shadow-xs hover:bg-primary/90', + destructive: + 'bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60', + outline: + 'border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50', + secondary: + 'bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80', + ghost: + 'hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50', + link: 'text-primary underline-offset-4 hover:underline', + }, + size: { + default: 'h-9 px-4 py-2 has-[>svg]:px-3', + sm: 'h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5', + lg: 'h-10 rounded-md px-6 has-[>svg]:px-4', + icon: 'size-9', + }, + }, + defaultVariants: { + variant: 'default', + size: 'default', + }, + }, +) + +export type ButtonVariants = VariantProps diff --git a/management-ui/src/components/ui/card/Card.vue b/management-ui/src/components/ui/card/Card.vue new file mode 100644 index 0000000..2b447cc --- /dev/null +++ b/management-ui/src/components/ui/card/Card.vue @@ -0,0 +1,22 @@ + + + diff --git a/management-ui/src/components/ui/card/CardAction.vue b/management-ui/src/components/ui/card/CardAction.vue new file mode 100644 index 0000000..77da12e --- /dev/null +++ b/management-ui/src/components/ui/card/CardAction.vue @@ -0,0 +1,17 @@ + + + diff --git a/management-ui/src/components/ui/card/CardContent.vue b/management-ui/src/components/ui/card/CardContent.vue new file mode 100644 index 0000000..6d330dc --- /dev/null +++ b/management-ui/src/components/ui/card/CardContent.vue @@ -0,0 +1,17 @@ + + + diff --git a/management-ui/src/components/ui/card/CardDescription.vue b/management-ui/src/components/ui/card/CardDescription.vue new file mode 100644 index 0000000..fef88d3 --- /dev/null +++ b/management-ui/src/components/ui/card/CardDescription.vue @@ -0,0 +1,17 @@ + + + diff --git a/management-ui/src/components/ui/card/CardFooter.vue b/management-ui/src/components/ui/card/CardFooter.vue new file mode 100644 index 0000000..5702cc1 --- /dev/null +++ b/management-ui/src/components/ui/card/CardFooter.vue @@ -0,0 +1,17 @@ + + + diff --git a/management-ui/src/components/ui/card/CardHeader.vue b/management-ui/src/components/ui/card/CardHeader.vue new file mode 100644 index 0000000..ad8c305 --- /dev/null +++ b/management-ui/src/components/ui/card/CardHeader.vue @@ -0,0 +1,17 @@ + + + diff --git a/management-ui/src/components/ui/card/CardTitle.vue b/management-ui/src/components/ui/card/CardTitle.vue new file mode 100644 index 0000000..1dc9bd3 --- /dev/null +++ b/management-ui/src/components/ui/card/CardTitle.vue @@ -0,0 +1,17 @@ + + + diff --git a/management-ui/src/components/ui/card/index.ts b/management-ui/src/components/ui/card/index.ts new file mode 100644 index 0000000..91680e1 --- /dev/null +++ b/management-ui/src/components/ui/card/index.ts @@ -0,0 +1,7 @@ +export { default as Card } from './Card.vue' +export { default as CardAction } from './CardAction.vue' +export { default as CardContent } from './CardContent.vue' +export { default as CardDescription } from './CardDescription.vue' +export { default as CardFooter } from './CardFooter.vue' +export { default as CardHeader } from './CardHeader.vue' +export { default as CardTitle } from './CardTitle.vue' diff --git a/management-ui/src/components/ui/combobox/Combobox.vue b/management-ui/src/components/ui/combobox/Combobox.vue new file mode 100644 index 0000000..75d8632 --- /dev/null +++ b/management-ui/src/components/ui/combobox/Combobox.vue @@ -0,0 +1,17 @@ + + + diff --git a/management-ui/src/components/ui/combobox/ComboboxAnchor.vue b/management-ui/src/components/ui/combobox/ComboboxAnchor.vue new file mode 100644 index 0000000..fbde937 --- /dev/null +++ b/management-ui/src/components/ui/combobox/ComboboxAnchor.vue @@ -0,0 +1,23 @@ + + + diff --git a/management-ui/src/components/ui/combobox/ComboboxEmpty.vue b/management-ui/src/components/ui/combobox/ComboboxEmpty.vue new file mode 100644 index 0000000..5765ee1 --- /dev/null +++ b/management-ui/src/components/ui/combobox/ComboboxEmpty.vue @@ -0,0 +1,21 @@ + + + diff --git a/management-ui/src/components/ui/combobox/ComboboxGroup.vue b/management-ui/src/components/ui/combobox/ComboboxGroup.vue new file mode 100644 index 0000000..b6e23fb --- /dev/null +++ b/management-ui/src/components/ui/combobox/ComboboxGroup.vue @@ -0,0 +1,27 @@ + + + diff --git a/management-ui/src/components/ui/combobox/ComboboxInput.vue b/management-ui/src/components/ui/combobox/ComboboxInput.vue new file mode 100644 index 0000000..7854cf1 --- /dev/null +++ b/management-ui/src/components/ui/combobox/ComboboxInput.vue @@ -0,0 +1,41 @@ + + + diff --git a/management-ui/src/components/ui/combobox/ComboboxItem.vue b/management-ui/src/components/ui/combobox/ComboboxItem.vue new file mode 100644 index 0000000..b23357b --- /dev/null +++ b/management-ui/src/components/ui/combobox/ComboboxItem.vue @@ -0,0 +1,24 @@ + + + diff --git a/management-ui/src/components/ui/combobox/ComboboxItemIndicator.vue b/management-ui/src/components/ui/combobox/ComboboxItemIndicator.vue new file mode 100644 index 0000000..d0858e8 --- /dev/null +++ b/management-ui/src/components/ui/combobox/ComboboxItemIndicator.vue @@ -0,0 +1,23 @@ + + + diff --git a/management-ui/src/components/ui/combobox/ComboboxList.vue b/management-ui/src/components/ui/combobox/ComboboxList.vue new file mode 100644 index 0000000..c77fde5 --- /dev/null +++ b/management-ui/src/components/ui/combobox/ComboboxList.vue @@ -0,0 +1,29 @@ + + + diff --git a/management-ui/src/components/ui/combobox/ComboboxSeparator.vue b/management-ui/src/components/ui/combobox/ComboboxSeparator.vue new file mode 100644 index 0000000..561eb7d --- /dev/null +++ b/management-ui/src/components/ui/combobox/ComboboxSeparator.vue @@ -0,0 +1,21 @@ + + + diff --git a/management-ui/src/components/ui/combobox/ComboboxTrigger.vue b/management-ui/src/components/ui/combobox/ComboboxTrigger.vue new file mode 100644 index 0000000..454f3f8 --- /dev/null +++ b/management-ui/src/components/ui/combobox/ComboboxTrigger.vue @@ -0,0 +1,24 @@ + + + diff --git a/management-ui/src/components/ui/combobox/ComboboxViewport.vue b/management-ui/src/components/ui/combobox/ComboboxViewport.vue new file mode 100644 index 0000000..3abd323 --- /dev/null +++ b/management-ui/src/components/ui/combobox/ComboboxViewport.vue @@ -0,0 +1,23 @@ + + + diff --git a/management-ui/src/components/ui/combobox/index.ts b/management-ui/src/components/ui/combobox/index.ts new file mode 100644 index 0000000..98fce96 --- /dev/null +++ b/management-ui/src/components/ui/combobox/index.ts @@ -0,0 +1,12 @@ +export { default as Combobox } from './Combobox.vue' +export { default as ComboboxAnchor } from './ComboboxAnchor.vue' +export { default as ComboboxEmpty } from './ComboboxEmpty.vue' +export { default as ComboboxGroup } from './ComboboxGroup.vue' +export { default as ComboboxInput } from './ComboboxInput.vue' +export { default as ComboboxItem } from './ComboboxItem.vue' +export { default as ComboboxItemIndicator } from './ComboboxItemIndicator.vue' +export { default as ComboboxList } from './ComboboxList.vue' +export { default as ComboboxSeparator } from './ComboboxSeparator.vue' +export { default as ComboboxViewport } from './ComboboxViewport.vue' + +export { ComboboxCancel, ComboboxTrigger } from 'reka-ui' diff --git a/management-ui/src/components/ui/dialog/Dialog.vue b/management-ui/src/components/ui/dialog/Dialog.vue new file mode 100644 index 0000000..d3ef205 --- /dev/null +++ b/management-ui/src/components/ui/dialog/Dialog.vue @@ -0,0 +1,17 @@ + + + diff --git a/management-ui/src/components/ui/dialog/DialogClose.vue b/management-ui/src/components/ui/dialog/DialogClose.vue new file mode 100644 index 0000000..ec17092 --- /dev/null +++ b/management-ui/src/components/ui/dialog/DialogClose.vue @@ -0,0 +1,14 @@ + + + diff --git a/management-ui/src/components/ui/dialog/DialogContent.vue b/management-ui/src/components/ui/dialog/DialogContent.vue new file mode 100644 index 0000000..e33fa3c --- /dev/null +++ b/management-ui/src/components/ui/dialog/DialogContent.vue @@ -0,0 +1,46 @@ + + + diff --git a/management-ui/src/components/ui/dialog/DialogDescription.vue b/management-ui/src/components/ui/dialog/DialogDescription.vue new file mode 100644 index 0000000..74e1a44 --- /dev/null +++ b/management-ui/src/components/ui/dialog/DialogDescription.vue @@ -0,0 +1,22 @@ + + + diff --git a/management-ui/src/components/ui/dialog/DialogFooter.vue b/management-ui/src/components/ui/dialog/DialogFooter.vue new file mode 100644 index 0000000..fd26d48 --- /dev/null +++ b/management-ui/src/components/ui/dialog/DialogFooter.vue @@ -0,0 +1,15 @@ + + + diff --git a/management-ui/src/components/ui/dialog/DialogHeader.vue b/management-ui/src/components/ui/dialog/DialogHeader.vue new file mode 100644 index 0000000..aa9ab52 --- /dev/null +++ b/management-ui/src/components/ui/dialog/DialogHeader.vue @@ -0,0 +1,17 @@ + + + diff --git a/management-ui/src/components/ui/dialog/DialogOverlay.vue b/management-ui/src/components/ui/dialog/DialogOverlay.vue new file mode 100644 index 0000000..1ff5b60 --- /dev/null +++ b/management-ui/src/components/ui/dialog/DialogOverlay.vue @@ -0,0 +1,20 @@ + + + diff --git a/management-ui/src/components/ui/dialog/DialogScrollContent.vue b/management-ui/src/components/ui/dialog/DialogScrollContent.vue new file mode 100644 index 0000000..09a29d4 --- /dev/null +++ b/management-ui/src/components/ui/dialog/DialogScrollContent.vue @@ -0,0 +1,56 @@ + + + diff --git a/management-ui/src/components/ui/dialog/DialogTitle.vue b/management-ui/src/components/ui/dialog/DialogTitle.vue new file mode 100644 index 0000000..10c81db --- /dev/null +++ b/management-ui/src/components/ui/dialog/DialogTitle.vue @@ -0,0 +1,22 @@ + + + diff --git a/management-ui/src/components/ui/dialog/DialogTrigger.vue b/management-ui/src/components/ui/dialog/DialogTrigger.vue new file mode 100644 index 0000000..8a64056 --- /dev/null +++ b/management-ui/src/components/ui/dialog/DialogTrigger.vue @@ -0,0 +1,14 @@ + + + diff --git a/management-ui/src/components/ui/dialog/index.ts b/management-ui/src/components/ui/dialog/index.ts new file mode 100644 index 0000000..6a679e7 --- /dev/null +++ b/management-ui/src/components/ui/dialog/index.ts @@ -0,0 +1,10 @@ +export { default as Dialog } from './Dialog.vue' +export { default as DialogClose } from './DialogClose.vue' +export { default as DialogContent } from './DialogContent.vue' +export { default as DialogDescription } from './DialogDescription.vue' +export { default as DialogFooter } from './DialogFooter.vue' +export { default as DialogHeader } from './DialogHeader.vue' +export { default as DialogOverlay } from './DialogOverlay.vue' +export { default as DialogScrollContent } from './DialogScrollContent.vue' +export { default as DialogTitle } from './DialogTitle.vue' +export { default as DialogTrigger } from './DialogTrigger.vue' diff --git a/management-ui/src/components/ui/dropdown-menu/DropdownMenu.vue b/management-ui/src/components/ui/dropdown-menu/DropdownMenu.vue new file mode 100644 index 0000000..05336c0 --- /dev/null +++ b/management-ui/src/components/ui/dropdown-menu/DropdownMenu.vue @@ -0,0 +1,17 @@ + + + diff --git a/management-ui/src/components/ui/dropdown-menu/DropdownMenuCheckboxItem.vue b/management-ui/src/components/ui/dropdown-menu/DropdownMenuCheckboxItem.vue new file mode 100644 index 0000000..8e5beb8 --- /dev/null +++ b/management-ui/src/components/ui/dropdown-menu/DropdownMenuCheckboxItem.vue @@ -0,0 +1,38 @@ + + + diff --git a/management-ui/src/components/ui/dropdown-menu/DropdownMenuContent.vue b/management-ui/src/components/ui/dropdown-menu/DropdownMenuContent.vue new file mode 100644 index 0000000..7381315 --- /dev/null +++ b/management-ui/src/components/ui/dropdown-menu/DropdownMenuContent.vue @@ -0,0 +1,36 @@ + + + diff --git a/management-ui/src/components/ui/dropdown-menu/DropdownMenuGroup.vue b/management-ui/src/components/ui/dropdown-menu/DropdownMenuGroup.vue new file mode 100644 index 0000000..c516269 --- /dev/null +++ b/management-ui/src/components/ui/dropdown-menu/DropdownMenuGroup.vue @@ -0,0 +1,14 @@ + + + diff --git a/management-ui/src/components/ui/dropdown-menu/DropdownMenuItem.vue b/management-ui/src/components/ui/dropdown-menu/DropdownMenuItem.vue new file mode 100644 index 0000000..f3cf879 --- /dev/null +++ b/management-ui/src/components/ui/dropdown-menu/DropdownMenuItem.vue @@ -0,0 +1,30 @@ + + + diff --git a/management-ui/src/components/ui/dropdown-menu/DropdownMenuLabel.vue b/management-ui/src/components/ui/dropdown-menu/DropdownMenuLabel.vue new file mode 100644 index 0000000..2612018 --- /dev/null +++ b/management-ui/src/components/ui/dropdown-menu/DropdownMenuLabel.vue @@ -0,0 +1,22 @@ + + + diff --git a/management-ui/src/components/ui/dropdown-menu/DropdownMenuRadioGroup.vue b/management-ui/src/components/ui/dropdown-menu/DropdownMenuRadioGroup.vue new file mode 100644 index 0000000..f3b0605 --- /dev/null +++ b/management-ui/src/components/ui/dropdown-menu/DropdownMenuRadioGroup.vue @@ -0,0 +1,22 @@ + + + diff --git a/management-ui/src/components/ui/dropdown-menu/DropdownMenuRadioItem.vue b/management-ui/src/components/ui/dropdown-menu/DropdownMenuRadioItem.vue new file mode 100644 index 0000000..e3f9666 --- /dev/null +++ b/management-ui/src/components/ui/dropdown-menu/DropdownMenuRadioItem.vue @@ -0,0 +1,39 @@ + + + diff --git a/management-ui/src/components/ui/dropdown-menu/DropdownMenuSeparator.vue b/management-ui/src/components/ui/dropdown-menu/DropdownMenuSeparator.vue new file mode 100644 index 0000000..1d9c347 --- /dev/null +++ b/management-ui/src/components/ui/dropdown-menu/DropdownMenuSeparator.vue @@ -0,0 +1,23 @@ + + + diff --git a/management-ui/src/components/ui/dropdown-menu/DropdownMenuShortcut.vue b/management-ui/src/components/ui/dropdown-menu/DropdownMenuShortcut.vue new file mode 100644 index 0000000..c04fc8f --- /dev/null +++ b/management-ui/src/components/ui/dropdown-menu/DropdownMenuShortcut.vue @@ -0,0 +1,17 @@ + + + diff --git a/management-ui/src/components/ui/dropdown-menu/DropdownMenuSub.vue b/management-ui/src/components/ui/dropdown-menu/DropdownMenuSub.vue new file mode 100644 index 0000000..e829f22 --- /dev/null +++ b/management-ui/src/components/ui/dropdown-menu/DropdownMenuSub.vue @@ -0,0 +1,19 @@ + + + diff --git a/management-ui/src/components/ui/dropdown-menu/DropdownMenuSubContent.vue b/management-ui/src/components/ui/dropdown-menu/DropdownMenuSubContent.vue new file mode 100644 index 0000000..9bdbeda --- /dev/null +++ b/management-ui/src/components/ui/dropdown-menu/DropdownMenuSubContent.vue @@ -0,0 +1,28 @@ + + + diff --git a/management-ui/src/components/ui/dropdown-menu/DropdownMenuSubTrigger.vue b/management-ui/src/components/ui/dropdown-menu/DropdownMenuSubTrigger.vue new file mode 100644 index 0000000..989baa6 --- /dev/null +++ b/management-ui/src/components/ui/dropdown-menu/DropdownMenuSubTrigger.vue @@ -0,0 +1,30 @@ + + + diff --git a/management-ui/src/components/ui/dropdown-menu/DropdownMenuTrigger.vue b/management-ui/src/components/ui/dropdown-menu/DropdownMenuTrigger.vue new file mode 100644 index 0000000..891a9dc --- /dev/null +++ b/management-ui/src/components/ui/dropdown-menu/DropdownMenuTrigger.vue @@ -0,0 +1,16 @@ + + + diff --git a/management-ui/src/components/ui/dropdown-menu/index.ts b/management-ui/src/components/ui/dropdown-menu/index.ts new file mode 100644 index 0000000..866da4e --- /dev/null +++ b/management-ui/src/components/ui/dropdown-menu/index.ts @@ -0,0 +1,16 @@ +export { default as DropdownMenu } from './DropdownMenu.vue' + +export { default as DropdownMenuCheckboxItem } from './DropdownMenuCheckboxItem.vue' +export { default as DropdownMenuContent } from './DropdownMenuContent.vue' +export { default as DropdownMenuGroup } from './DropdownMenuGroup.vue' +export { default as DropdownMenuItem } from './DropdownMenuItem.vue' +export { default as DropdownMenuLabel } from './DropdownMenuLabel.vue' +export { default as DropdownMenuRadioGroup } from './DropdownMenuRadioGroup.vue' +export { default as DropdownMenuRadioItem } from './DropdownMenuRadioItem.vue' +export { default as DropdownMenuSeparator } from './DropdownMenuSeparator.vue' +export { default as DropdownMenuShortcut } from './DropdownMenuShortcut.vue' +export { default as DropdownMenuSub } from './DropdownMenuSub.vue' +export { default as DropdownMenuSubContent } from './DropdownMenuSubContent.vue' +export { default as DropdownMenuSubTrigger } from './DropdownMenuSubTrigger.vue' +export { default as DropdownMenuTrigger } from './DropdownMenuTrigger.vue' +export { DropdownMenuPortal } from 'reka-ui' diff --git a/management-ui/src/components/ui/input/Input.vue b/management-ui/src/components/ui/input/Input.vue new file mode 100644 index 0000000..d1a3642 --- /dev/null +++ b/management-ui/src/components/ui/input/Input.vue @@ -0,0 +1,33 @@ + + + diff --git a/management-ui/src/components/ui/input/index.ts b/management-ui/src/components/ui/input/index.ts new file mode 100644 index 0000000..a691dd6 --- /dev/null +++ b/management-ui/src/components/ui/input/index.ts @@ -0,0 +1 @@ +export { default as Input } from './Input.vue' diff --git a/management-ui/src/components/ui/label/Label.vue b/management-ui/src/components/ui/label/Label.vue new file mode 100644 index 0000000..e1ce62f --- /dev/null +++ b/management-ui/src/components/ui/label/Label.vue @@ -0,0 +1,25 @@ + + + diff --git a/management-ui/src/components/ui/label/index.ts b/management-ui/src/components/ui/label/index.ts new file mode 100644 index 0000000..572c2f0 --- /dev/null +++ b/management-ui/src/components/ui/label/index.ts @@ -0,0 +1 @@ +export { default as Label } from './Label.vue' diff --git a/management-ui/src/components/ui/scroll-area/ScrollArea.vue b/management-ui/src/components/ui/scroll-area/ScrollArea.vue new file mode 100644 index 0000000..9f6cbf5 --- /dev/null +++ b/management-ui/src/components/ui/scroll-area/ScrollArea.vue @@ -0,0 +1,33 @@ + + + diff --git a/management-ui/src/components/ui/scroll-area/ScrollBar.vue b/management-ui/src/components/ui/scroll-area/ScrollBar.vue new file mode 100644 index 0000000..f738101 --- /dev/null +++ b/management-ui/src/components/ui/scroll-area/ScrollBar.vue @@ -0,0 +1,31 @@ + + + diff --git a/management-ui/src/components/ui/scroll-area/index.ts b/management-ui/src/components/ui/scroll-area/index.ts new file mode 100644 index 0000000..448071d --- /dev/null +++ b/management-ui/src/components/ui/scroll-area/index.ts @@ -0,0 +1,2 @@ +export { default as ScrollArea } from './ScrollArea.vue' +export { default as ScrollBar } from './ScrollBar.vue' diff --git a/management-ui/src/components/ui/separator/Separator.vue b/management-ui/src/components/ui/separator/Separator.vue new file mode 100644 index 0000000..c8f0500 --- /dev/null +++ b/management-ui/src/components/ui/separator/Separator.vue @@ -0,0 +1,28 @@ + + + diff --git a/management-ui/src/components/ui/separator/index.ts b/management-ui/src/components/ui/separator/index.ts new file mode 100644 index 0000000..2287bcb --- /dev/null +++ b/management-ui/src/components/ui/separator/index.ts @@ -0,0 +1 @@ +export { default as Separator } from './Separator.vue' diff --git a/management-ui/src/components/ui/switch/Switch.vue b/management-ui/src/components/ui/switch/Switch.vue new file mode 100644 index 0000000..aac7adf --- /dev/null +++ b/management-ui/src/components/ui/switch/Switch.vue @@ -0,0 +1,38 @@ + + + diff --git a/management-ui/src/components/ui/switch/index.ts b/management-ui/src/components/ui/switch/index.ts new file mode 100644 index 0000000..87b4b17 --- /dev/null +++ b/management-ui/src/components/ui/switch/index.ts @@ -0,0 +1 @@ +export { default as Switch } from './Switch.vue' diff --git a/management-ui/src/components/ui/table/Table.vue b/management-ui/src/components/ui/table/Table.vue new file mode 100644 index 0000000..050f67a --- /dev/null +++ b/management-ui/src/components/ui/table/Table.vue @@ -0,0 +1,16 @@ + + + diff --git a/management-ui/src/components/ui/table/TableBody.vue b/management-ui/src/components/ui/table/TableBody.vue new file mode 100644 index 0000000..94c4f7f --- /dev/null +++ b/management-ui/src/components/ui/table/TableBody.vue @@ -0,0 +1,17 @@ + + + diff --git a/management-ui/src/components/ui/table/TableCaption.vue b/management-ui/src/components/ui/table/TableCaption.vue new file mode 100644 index 0000000..7d0016c --- /dev/null +++ b/management-ui/src/components/ui/table/TableCaption.vue @@ -0,0 +1,17 @@ + + + diff --git a/management-ui/src/components/ui/table/TableCell.vue b/management-ui/src/components/ui/table/TableCell.vue new file mode 100644 index 0000000..de766c6 --- /dev/null +++ b/management-ui/src/components/ui/table/TableCell.vue @@ -0,0 +1,22 @@ + + + diff --git a/management-ui/src/components/ui/table/TableEmpty.vue b/management-ui/src/components/ui/table/TableEmpty.vue new file mode 100644 index 0000000..42c0e24 --- /dev/null +++ b/management-ui/src/components/ui/table/TableEmpty.vue @@ -0,0 +1,34 @@ + + + diff --git a/management-ui/src/components/ui/table/TableFooter.vue b/management-ui/src/components/ui/table/TableFooter.vue new file mode 100644 index 0000000..5f90a2b --- /dev/null +++ b/management-ui/src/components/ui/table/TableFooter.vue @@ -0,0 +1,17 @@ + + + diff --git a/management-ui/src/components/ui/table/TableHead.vue b/management-ui/src/components/ui/table/TableHead.vue new file mode 100644 index 0000000..8e32dd4 --- /dev/null +++ b/management-ui/src/components/ui/table/TableHead.vue @@ -0,0 +1,17 @@ + + + diff --git a/management-ui/src/components/ui/table/TableHeader.vue b/management-ui/src/components/ui/table/TableHeader.vue new file mode 100644 index 0000000..69cf93d --- /dev/null +++ b/management-ui/src/components/ui/table/TableHeader.vue @@ -0,0 +1,17 @@ + + + diff --git a/management-ui/src/components/ui/table/TableRow.vue b/management-ui/src/components/ui/table/TableRow.vue new file mode 100644 index 0000000..1a9cd78 --- /dev/null +++ b/management-ui/src/components/ui/table/TableRow.vue @@ -0,0 +1,17 @@ + + + diff --git a/management-ui/src/components/ui/table/index.ts b/management-ui/src/components/ui/table/index.ts new file mode 100644 index 0000000..67f382c --- /dev/null +++ b/management-ui/src/components/ui/table/index.ts @@ -0,0 +1,9 @@ +export { default as Table } from './Table.vue' +export { default as TableBody } from './TableBody.vue' +export { default as TableCaption } from './TableCaption.vue' +export { default as TableCell } from './TableCell.vue' +export { default as TableEmpty } from './TableEmpty.vue' +export { default as TableFooter } from './TableFooter.vue' +export { default as TableHead } from './TableHead.vue' +export { default as TableHeader } from './TableHeader.vue' +export { default as TableRow } from './TableRow.vue' diff --git a/management-ui/src/components/ui/table/utils.ts b/management-ui/src/components/ui/table/utils.ts new file mode 100644 index 0000000..8ed839b --- /dev/null +++ b/management-ui/src/components/ui/table/utils.ts @@ -0,0 +1,9 @@ +import type { Updater } from '@tanstack/vue-table' +import type { Ref } from 'vue' + +export function valueUpdater>(updaterOrValue: T, ref: Ref) { + ref.value + = typeof updaterOrValue === 'function' + ? updaterOrValue(ref.value) + : updaterOrValue +} diff --git a/management-ui/src/components/ui/tabs/Tabs.vue b/management-ui/src/components/ui/tabs/Tabs.vue new file mode 100644 index 0000000..f887a2c --- /dev/null +++ b/management-ui/src/components/ui/tabs/Tabs.vue @@ -0,0 +1,23 @@ + + + diff --git a/management-ui/src/components/ui/tabs/TabsContent.vue b/management-ui/src/components/ui/tabs/TabsContent.vue new file mode 100644 index 0000000..80c78ed --- /dev/null +++ b/management-ui/src/components/ui/tabs/TabsContent.vue @@ -0,0 +1,20 @@ + + + diff --git a/management-ui/src/components/ui/tabs/TabsList.vue b/management-ui/src/components/ui/tabs/TabsList.vue new file mode 100644 index 0000000..bf8cb97 --- /dev/null +++ b/management-ui/src/components/ui/tabs/TabsList.vue @@ -0,0 +1,23 @@ + + + diff --git a/management-ui/src/components/ui/tabs/TabsTrigger.vue b/management-ui/src/components/ui/tabs/TabsTrigger.vue new file mode 100644 index 0000000..dd25c67 --- /dev/null +++ b/management-ui/src/components/ui/tabs/TabsTrigger.vue @@ -0,0 +1,25 @@ + + + diff --git a/management-ui/src/components/ui/tabs/index.ts b/management-ui/src/components/ui/tabs/index.ts new file mode 100644 index 0000000..d56c937 --- /dev/null +++ b/management-ui/src/components/ui/tabs/index.ts @@ -0,0 +1,4 @@ +export { default as Tabs } from './Tabs.vue' +export { default as TabsContent } from './TabsContent.vue' +export { default as TabsList } from './TabsList.vue' +export { default as TabsTrigger } from './TabsTrigger.vue' diff --git a/management-ui/src/components/ui/tags-input/TagsInput.vue b/management-ui/src/components/ui/tags-input/TagsInput.vue new file mode 100644 index 0000000..3d59bb8 --- /dev/null +++ b/management-ui/src/components/ui/tags-input/TagsInput.vue @@ -0,0 +1,19 @@ + + + diff --git a/management-ui/src/components/ui/tags-input/TagsInputInput.vue b/management-ui/src/components/ui/tags-input/TagsInputInput.vue new file mode 100644 index 0000000..f3c245b --- /dev/null +++ b/management-ui/src/components/ui/tags-input/TagsInputInput.vue @@ -0,0 +1,16 @@ + + + diff --git a/management-ui/src/components/ui/tags-input/TagsInputItem.vue b/management-ui/src/components/ui/tags-input/TagsInputItem.vue new file mode 100644 index 0000000..7ffd2b5 --- /dev/null +++ b/management-ui/src/components/ui/tags-input/TagsInputItem.vue @@ -0,0 +1,19 @@ + + + diff --git a/management-ui/src/components/ui/tags-input/TagsInputItemDelete.vue b/management-ui/src/components/ui/tags-input/TagsInputItemDelete.vue new file mode 100644 index 0000000..fcd3e7a --- /dev/null +++ b/management-ui/src/components/ui/tags-input/TagsInputItemDelete.vue @@ -0,0 +1,21 @@ + + + diff --git a/management-ui/src/components/ui/tags-input/TagsInputItemText.vue b/management-ui/src/components/ui/tags-input/TagsInputItemText.vue new file mode 100644 index 0000000..0b2d91a --- /dev/null +++ b/management-ui/src/components/ui/tags-input/TagsInputItemText.vue @@ -0,0 +1,16 @@ + + + diff --git a/management-ui/src/components/ui/tags-input/index.ts b/management-ui/src/components/ui/tags-input/index.ts new file mode 100644 index 0000000..4e921e4 --- /dev/null +++ b/management-ui/src/components/ui/tags-input/index.ts @@ -0,0 +1,5 @@ +export { default as TagsInput } from './TagsInput.vue' +export { default as TagsInputInput } from './TagsInputInput.vue' +export { default as TagsInputItem } from './TagsInputItem.vue' +export { default as TagsInputItemDelete } from './TagsInputItemDelete.vue' +export { default as TagsInputItemText } from './TagsInputItemText.vue' diff --git a/management-ui/src/customcompometns/AdminDeviceDropdown.vue b/management-ui/src/customcompometns/AdminDeviceDropdown.vue new file mode 100644 index 0000000..1537634 --- /dev/null +++ b/management-ui/src/customcompometns/AdminDeviceDropdown.vue @@ -0,0 +1,40 @@ + + \ No newline at end of file diff --git a/management-ui/src/customcompometns/AdminUserDropdonw.vue b/management-ui/src/customcompometns/AdminUserDropdonw.vue new file mode 100644 index 0000000..4ad8cfc --- /dev/null +++ b/management-ui/src/customcompometns/AdminUserDropdonw.vue @@ -0,0 +1,44 @@ + + \ No newline at end of file diff --git a/management-ui/src/customcompometns/Admincomponent.vue b/management-ui/src/customcompometns/Admincomponent.vue new file mode 100644 index 0000000..c75fd80 --- /dev/null +++ b/management-ui/src/customcompometns/Admincomponent.vue @@ -0,0 +1,154 @@ + + + \ No newline at end of file diff --git a/management-ui/src/customcompometns/AssignDevice.vue b/management-ui/src/customcompometns/AssignDevice.vue new file mode 100644 index 0000000..8090cc4 --- /dev/null +++ b/management-ui/src/customcompometns/AssignDevice.vue @@ -0,0 +1,134 @@ + + + diff --git a/management-ui/src/customcompometns/CreateDevice.vue b/management-ui/src/customcompometns/CreateDevice.vue new file mode 100644 index 0000000..fccd780 --- /dev/null +++ b/management-ui/src/customcompometns/CreateDevice.vue @@ -0,0 +1,82 @@ + + \ No newline at end of file diff --git a/management-ui/src/customcompometns/CreateUser.vue b/management-ui/src/customcompometns/CreateUser.vue new file mode 100644 index 0000000..b9900f4 --- /dev/null +++ b/management-ui/src/customcompometns/CreateUser.vue @@ -0,0 +1,67 @@ + + \ No newline at end of file diff --git a/management-ui/src/customcompometns/DataTableNoCheckbox.vue b/management-ui/src/customcompometns/DataTableNoCheckbox.vue new file mode 100644 index 0000000..1fe44d5 --- /dev/null +++ b/management-ui/src/customcompometns/DataTableNoCheckbox.vue @@ -0,0 +1,120 @@ + + + diff --git a/management-ui/src/customcompometns/DeleteDeviceDialog.vue b/management-ui/src/customcompometns/DeleteDeviceDialog.vue new file mode 100644 index 0000000..195e68e --- /dev/null +++ b/management-ui/src/customcompometns/DeleteDeviceDialog.vue @@ -0,0 +1,47 @@ + + + \ No newline at end of file diff --git a/management-ui/src/customcompometns/DeleteUserDialog.vue b/management-ui/src/customcompometns/DeleteUserDialog.vue new file mode 100644 index 0000000..414a6ff --- /dev/null +++ b/management-ui/src/customcompometns/DeleteUserDialog.vue @@ -0,0 +1,47 @@ + + + \ No newline at end of file diff --git a/management-ui/src/customcompometns/DeviceComponent.vue b/management-ui/src/customcompometns/DeviceComponent.vue new file mode 100644 index 0000000..a5d6645 --- /dev/null +++ b/management-ui/src/customcompometns/DeviceComponent.vue @@ -0,0 +1,22 @@ + + \ No newline at end of file diff --git a/management-ui/src/customcompometns/DevicesGrid.vue b/management-ui/src/customcompometns/DevicesGrid.vue new file mode 100644 index 0000000..6d272b4 --- /dev/null +++ b/management-ui/src/customcompometns/DevicesGrid.vue @@ -0,0 +1,97 @@ + + + + + \ No newline at end of file diff --git a/management-ui/src/customcompometns/EditDeviceDialog.vue b/management-ui/src/customcompometns/EditDeviceDialog.vue new file mode 100644 index 0000000..7cb6773 --- /dev/null +++ b/management-ui/src/customcompometns/EditDeviceDialog.vue @@ -0,0 +1,71 @@ + + + \ No newline at end of file diff --git a/management-ui/src/customcompometns/EditUserDialog.vue b/management-ui/src/customcompometns/EditUserDialog.vue new file mode 100644 index 0000000..82b0af4 --- /dev/null +++ b/management-ui/src/customcompometns/EditUserDialog.vue @@ -0,0 +1,71 @@ + + + \ No newline at end of file diff --git a/management-ui/src/customcompometns/Navbar.vue b/management-ui/src/customcompometns/Navbar.vue new file mode 100644 index 0000000..91e80ab --- /dev/null +++ b/management-ui/src/customcompometns/Navbar.vue @@ -0,0 +1,57 @@ + + + \ No newline at end of file diff --git a/management-ui/src/customcompometns/UserSettings.vue b/management-ui/src/customcompometns/UserSettings.vue new file mode 100644 index 0000000..490b77e --- /dev/null +++ b/management-ui/src/customcompometns/UserSettings.vue @@ -0,0 +1,37 @@ + + \ No newline at end of file diff --git a/management-ui/src/lib/api.ts b/management-ui/src/lib/api.ts new file mode 100644 index 0000000..53e6c88 --- /dev/null +++ b/management-ui/src/lib/api.ts @@ -0,0 +1,28 @@ +import axios from 'axios' +import { auth } from './auth' + +export const api = axios.create({ + baseURL: import.meta.env.VITE_API_URL || '/api', + timeout: 10_000, + headers: { 'Content-Type': 'application/json' }, + // withCredentials: false // bearer tokens don't need cookies +}) + +api.interceptors.request.use((config) => { + const t = auth.token.value + if (t) config.headers.Authorization = `Bearer ${t}` + return config +}) + +api.interceptors.response.use( + (res) => res, + (err) => { + if (err?.response?.status === 401) { + auth.clear() + if (window.location.pathname !== '/login') { + window.location.href = '/login' + } + } + return Promise.reject(err) + } +) \ No newline at end of file diff --git a/management-ui/src/lib/auth.ts b/management-ui/src/lib/auth.ts new file mode 100644 index 0000000..057be2f --- /dev/null +++ b/management-ui/src/lib/auth.ts @@ -0,0 +1,61 @@ +import { ref, computed } from 'vue' + +export type JwtPayload = { + sub?: string + exp?: number + iat?: number + role?: string | string[] + roles?: string[] + scope?: string + [k: string]: unknown +} + +function decodeJwt(token: string): JwtPayload | null { + try { + const [, payload] = token.split('.') + if (!payload) return null + // URL-safe Base64 → Base64 + const base64 = payload.replace(/-/g, '+').replace(/_/g, '/') + const json = decodeURIComponent( + atob(base64) + .split('') + .map((c) => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2)) + .join('') + ) + return JSON.parse(json) + } catch { + return null + } +} + +const token = ref(sessionStorage.getItem('accessToken')) +const payload = ref(token.value ? decodeJwt(token.value) : null) + +const isAuthenticated = computed(() => { + if (!token.value) return false + const exp = payload.value?.exp + return !exp || exp * 1000 > Date.now() +}) + +const roles = computed(() => { + const p = payload.value + if (!p) return [] + if (Array.isArray(p.roles)) return p.roles + if (typeof p.role === 'string') return [p.role] + if (typeof p.scope === 'string') return p.scope.split(' ') // e.g., "admin user" + return [] +}) + +function setToken(t: string) { + token.value = t + payload.value = decodeJwt(t) + sessionStorage.setItem('accessToken', t) +} + +function clear() { + token.value = null + payload.value = null + sessionStorage.removeItem('accessToken') +} + +export const auth = { token, payload, roles, isAuthenticated, setToken, clear } \ No newline at end of file diff --git a/management-ui/src/lib/interfaces.ts b/management-ui/src/lib/interfaces.ts new file mode 100644 index 0000000..4c1552b --- /dev/null +++ b/management-ui/src/lib/interfaces.ts @@ -0,0 +1,11 @@ +export interface Device { + guid: string + devicename: string + assigned_users: string +} + +export interface Users { + id: number, + username: string, + role: string +} \ No newline at end of file diff --git a/management-ui/src/lib/utils.ts b/management-ui/src/lib/utils.ts new file mode 100644 index 0000000..28f8215 --- /dev/null +++ b/management-ui/src/lib/utils.ts @@ -0,0 +1,6 @@ +import { type ClassValue, clsx } from 'clsx' +import { twMerge } from 'tailwind-merge' + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)) +} diff --git a/management-ui/src/main.ts b/management-ui/src/main.ts new file mode 100644 index 0000000..7b19912 --- /dev/null +++ b/management-ui/src/main.ts @@ -0,0 +1,6 @@ +import { createApp } from 'vue' +import './style.css' +import App from './App.vue' +import router from './router' + +createApp(App).use(router).mount('#app') diff --git a/management-ui/src/pages/Admin.vue b/management-ui/src/pages/Admin.vue new file mode 100644 index 0000000..06ebb8c --- /dev/null +++ b/management-ui/src/pages/Admin.vue @@ -0,0 +1,14 @@ + + + \ No newline at end of file diff --git a/management-ui/src/pages/Create.vue b/management-ui/src/pages/Create.vue new file mode 100644 index 0000000..cfdc099 --- /dev/null +++ b/management-ui/src/pages/Create.vue @@ -0,0 +1,246 @@ + + + \ No newline at end of file diff --git a/management-ui/src/pages/DeviceView.vue b/management-ui/src/pages/DeviceView.vue new file mode 100644 index 0000000..53c087b --- /dev/null +++ b/management-ui/src/pages/DeviceView.vue @@ -0,0 +1,13 @@ + + + \ No newline at end of file diff --git a/management-ui/src/pages/Devices.vue b/management-ui/src/pages/Devices.vue new file mode 100644 index 0000000..a8de1df --- /dev/null +++ b/management-ui/src/pages/Devices.vue @@ -0,0 +1,22 @@ + + + \ No newline at end of file diff --git a/management-ui/src/pages/Forbidden.vue b/management-ui/src/pages/Forbidden.vue new file mode 100644 index 0000000..694cedc --- /dev/null +++ b/management-ui/src/pages/Forbidden.vue @@ -0,0 +1,12 @@ + + \ No newline at end of file diff --git a/management-ui/src/pages/Login.vue b/management-ui/src/pages/Login.vue new file mode 100644 index 0000000..25ed3af --- /dev/null +++ b/management-ui/src/pages/Login.vue @@ -0,0 +1,82 @@ + + + \ No newline at end of file diff --git a/management-ui/src/pages/Settings.vue b/management-ui/src/pages/Settings.vue new file mode 100644 index 0000000..be0a2a2 --- /dev/null +++ b/management-ui/src/pages/Settings.vue @@ -0,0 +1,12 @@ + + + \ No newline at end of file diff --git a/management-ui/src/router.ts b/management-ui/src/router.ts new file mode 100644 index 0000000..0900530 --- /dev/null +++ b/management-ui/src/router.ts @@ -0,0 +1,99 @@ +import { createRouter, createWebHistory } from 'vue-router'; + +import Admin from '@/pages/Admin.vue'; +import Login from '@/pages/Login.vue'; +import Settings from '@/pages/Settings.vue'; +import Devices from '@/pages/Devices.vue'; +import DeviceView from './pages/DeviceView.vue'; +import Forbidden from './pages/Forbidden.vue'; +import Create from './pages/Create.vue'; +import { auth } from './lib/auth'; + + +declare module 'vue-router' { + interface RouteMeta { + requiresAuth?: boolean + roles?: string[] // allowed roles + } +} + +const routes = [ + { + path: '/login', + name: 'Login', + component: Login, + meta: { requiresAuth: false } + }, + { + path: '/', + redirect: '/devices', // Optional: Redirect the root path to /home + }, + { + path: '/devices', + name: 'Devices', + component: Devices, + meta: { requiresAuth: true } + }, + { + path: '/settings', + name: 'Settings', + component: Settings, + meta: { requiresAuth: true } + }, + { + path: '/admin', + name: 'Admin', + component: Admin, + meta: { requiresAuth: true, roles: ['admin'] } + }, + { + path: '/device/:guid', // ← new dynamic segment + name: 'DeviceView', + component: DeviceView, + props: true, // so `guid` shows up as a prop + meta: { requiresAuth: true } + }, + { + path: '/forbidden', + name: 'Forbidden', + component: Forbidden, + meta: { requiresAuth: false } + }, + { + path: '/create', + name: 'Create', + component: Create, + meta: { requiresAuth: true, roles: ['admin'] } + } +] + +const router = createRouter({ + history: createWebHistory(import.meta.env.BASE_URL), + routes, +}) + +// Navigation guard for protected routes. +// For any route except "Login", we force a full page load so that the server-side session check occurs. +router.beforeEach((to, _from, next) => { + const requiresAuth = to.meta.requiresAuth !== false && to.name !== 'Login' + + // 1) Not authenticated → /login?redirect= + if (requiresAuth && !auth.isAuthenticated.value) { + return next({ name: 'Login', query: { redirect: to.fullPath } }) + } + + // 2) Role check (if any) + const allowed = to.meta.roles + if (allowed && allowed.length > 0) { + const hasRole = allowed.some((r) => auth.roles.value.includes(r)) + if (!hasRole) { + return next({ name: 'Forbidden' }) + } + } + + next() +}) + + +export default router + diff --git a/management-ui/src/style.css b/management-ui/src/style.css new file mode 100644 index 0000000..0a61557 --- /dev/null +++ b/management-ui/src/style.css @@ -0,0 +1,123 @@ +@import "tailwindcss"; +@import "tw-animate-css"; + +@custom-variant dark (&:is(.dark *)); + +@theme inline { + --color-background: var(--background); + --color-foreground: var(--foreground); + --color-card: var(--card); + --color-card-foreground: var(--card-foreground); + --color-popover: var(--popover); + --color-popover-foreground: var(--popover-foreground); + --color-primary: var(--primary); + --color-primary-foreground: var(--primary-foreground); + --color-secondary: var(--secondary); + --color-secondary-foreground: var(--secondary-foreground); + --color-muted: var(--muted); + --color-muted-foreground: var(--muted-foreground); + --color-accent: var(--accent); + --color-accent-foreground: var(--accent-foreground); + --color-destructive: var(--destructive); + --color-destructive-foreground: var(--destructive-foreground); + --color-border: var(--border); + --color-input: var(--input); + --color-ring: var(--ring); + --color-chart-1: var(--chart-1); + --color-chart-2: var(--chart-2); + --color-chart-3: var(--chart-3); + --color-chart-4: var(--chart-4); + --color-chart-5: var(--chart-5); + --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); + --color-sidebar: var(--sidebar); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-ring: var(--sidebar-ring); +} + +:root { + --background: oklch(1 0 0); + --foreground: oklch(0.145 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0.145 0 0); + --popover: oklch(1 0 0); + --popover-foreground: oklch(0.145 0 0); + --primary: oklch(0.205 0 0); + --primary-foreground: oklch(0.985 0 0); + --secondary: oklch(0.97 0 0); + --secondary-foreground: oklch(0.205 0 0); + --muted: oklch(0.97 0 0); + --muted-foreground: oklch(0.556 0 0); + --accent: oklch(0.97 0 0); + --accent-foreground: oklch(0.205 0 0); + --destructive: oklch(0.577 0.245 27.325); + --destructive-foreground: oklch(0.577 0.245 27.325); + --border: oklch(0.922 0 0); + --input: oklch(0.922 0 0); + --ring: oklch(0.708 0 0); + --chart-1: oklch(0.646 0.222 41.116); + --chart-2: oklch(0.6 0.118 184.704); + --chart-3: oklch(0.398 0.07 227.392); + --chart-4: oklch(0.828 0.189 84.429); + --chart-5: oklch(0.769 0.188 70.08); + --radius: 0.625rem; + --sidebar: oklch(0.985 0 0); + --sidebar-foreground: oklch(0.145 0 0); + --sidebar-primary: oklch(0.205 0 0); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.97 0 0); + --sidebar-accent-foreground: oklch(0.205 0 0); + --sidebar-border: oklch(0.922 0 0); + --sidebar-ring: oklch(0.708 0 0); +} + +.dark { + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.145 0 0); + --card-foreground: oklch(0.985 0 0); + --popover: oklch(0.145 0 0); + --popover-foreground: oklch(0.985 0 0); + --primary: oklch(0.985 0 0); + --primary-foreground: oklch(0.205 0 0); + --secondary: oklch(0.269 0 0); + --secondary-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --accent: oklch(0.269 0 0); + --accent-foreground: oklch(0.985 0 0); + --destructive: oklch(0.396 0.141 25.723); + --destructive-foreground: oklch(0.637 0.237 25.331); + --border: oklch(0.269 0 0); + --input: oklch(0.269 0 0); + --ring: oklch(0.439 0 0); + --chart-1: oklch(0.488 0.243 264.376); + --chart-2: oklch(0.696 0.17 162.48); + --chart-3: oklch(0.769 0.188 70.08); + --chart-4: oklch(0.627 0.265 303.9); + --chart-5: oklch(0.645 0.246 16.439); + --sidebar: oklch(0.205 0 0); + --sidebar-foreground: oklch(0.985 0 0); + --sidebar-primary: oklch(0.488 0.243 264.376); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.269 0 0); + --sidebar-accent-foreground: oklch(0.985 0 0); + --sidebar-border: oklch(0.269 0 0); + --sidebar-ring: oklch(0.439 0 0); +} + +@layer base { + * { + @apply border-border outline-ring/50; + } + body { + @apply bg-background text-foreground; + } +} \ No newline at end of file diff --git a/management-ui/src/vite-env.d.ts b/management-ui/src/vite-env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/management-ui/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/management-ui/tsconfig.app.json b/management-ui/tsconfig.app.json new file mode 100644 index 0000000..31b4897 --- /dev/null +++ b/management-ui/tsconfig.app.json @@ -0,0 +1,22 @@ +{ + "extends": "@vue/tsconfig/tsconfig.dom.json", + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + + "baseUrl": ".", + "paths": { + "@/*": [ + "./src/*" + ] + }, + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"] +} diff --git a/management-ui/tsconfig.json b/management-ui/tsconfig.json new file mode 100644 index 0000000..856b926 --- /dev/null +++ b/management-ui/tsconfig.json @@ -0,0 +1,13 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ], + "compilerOptions": { + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"] + } + } +} diff --git a/management-ui/tsconfig.node.json b/management-ui/tsconfig.node.json new file mode 100644 index 0000000..f85a399 --- /dev/null +++ b/management-ui/tsconfig.node.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "target": "ES2023", + "lib": ["ES2023"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/management-ui/vite.config.ts b/management-ui/vite.config.ts new file mode 100644 index 0000000..de2b9d5 --- /dev/null +++ b/management-ui/vite.config.ts @@ -0,0 +1,26 @@ +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' +import path from 'node:path' +import tailwindcss from '@tailwindcss/vite' + +// https://vite.dev/config/ +export default defineConfig({ + plugins: [vue(),tailwindcss()], + resolve: { + alias: { + '@': path.resolve(__dirname, './src'), + }, + }, + server: { + host: true, // listen on 0.0.0.0 inside container + port: 5173, + // Allow the container DNS name and local access + allowedHosts: ['localhost', '127.0.0.1', 'web', 'nginx'], + // Make HMR use the public port (80) when going through Nginx + hmr: { + clientPort: 80, // browser connects to ws://:80/… + protocol: 'ws', // dev over HTTP + host: 'localhost', // adjust if you open via a LAN IP + }, + }, +}) diff --git a/nginx/dev.conf b/nginx/dev.conf new file mode 100644 index 0000000..38d8882 --- /dev/null +++ b/nginx/dev.conf @@ -0,0 +1,43 @@ +map $http_upgrade $connection_upgrade { + default upgrade; + '' close; +} + +# Helpful for larger uploads via API (tweak as you wish) +client_max_body_size 400m; + +server { + listen 80; + server_name _; + + # Common proxy settings + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + # --- Frontend (Vite dev server) --- + # Pass EVERYTHING else to Vite dev server on `web:5173` + # Includes HMR websocket support. + location / { + proxy_pass http://web:5173; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; + proxy_read_timeout 3600s; + proxy_send_timeout 3600s; + } + + # --- API --- + # Strip the /api prefix (so backend sees /foo instead of /api/foo) + location /api/ { + proxy_pass http://snoop-api:8080/; # note the trailing slash -> strips /api + proxy_http_version 1.1; + + # WebSocket/SSE-friendly defaults for API (covers /api/ws etc.) + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; + proxy_read_timeout 3600s; + proxy_send_timeout 3600s; + } +} \ No newline at end of file diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..5094417 --- /dev/null +++ b/readme.md @@ -0,0 +1,38 @@ +# Vault setup + +```bash +export VAULT_ADDR=http://localhost:8200 +export VAULT_TOKEN=root + +# Enable KV v2 (if not already): vault secrets enable -path=kv kv-v2 +vault secrets enable -path=kv kv-v2 + +# Put secrets (example) +vault kv put kv/snoop \ + db_dsn="postgres://snoop:snoop@postgres:5432/snoop?sslmode=disable" \ + minio_endpoint="minio:9000" \ + minio_access_key="minioadmin" \ + minio_secret_key="minioadmin" \ + minio_use_ssl="false" \ + jwt_secret="supersecretjwt" \ + minio_records_bucket="records" \ + minio_livestream_bucket="livestream" \ + minio_presign_ttl_seconds="900" +``` + +Unseal Key: gQrvvJBaGR4CpmkoVq93tWqk5dTpioMAHtNHKMWNlH0= + +Root Token: root + + +{ + "db_dsn": "postgres://snoop:snoop@postgres:5432/snoop?sslmode=disable", + "minio_endpoint": "minio:9000", + "minio_access_key": "minioadmin", + "minio_secret_key": "minioadmin", + "minio_use_ssl": "false", + "jwt_secret": "supersecretjwt", + "minio_records_bucket": "records", + "minio_livestream_bucket": "livestream", + "minio_presign_ttl_seconds": "900" +} \ No newline at end of file diff --git a/server/Dockerfile b/server/Dockerfile new file mode 100644 index 0000000..9e8f101 --- /dev/null +++ b/server/Dockerfile @@ -0,0 +1,36 @@ +######################## +# 1) Build stage +######################## +FROM --platform=$BUILDPLATFORM golang:1.23-alpine AS build +WORKDIR /src +ENV CGO_ENABLED=0 + +# CA certs for TLS to Vault/MinIO +RUN apk add --no-cache ca-certificates && update-ca-certificates + +# Cache deps +COPY go.mod go.sum ./ +RUN --mount=type=cache,target=/go/pkg/mod \ + go mod download + +# Copy source +COPY . . + +# Pick your entrypoint package; default assumes ./cmd/api/main.go +# You can override APP_DIR build-arg from compose if needed. +ARG APP_DIR=./cmd/api +ARG TARGETOS TARGETARCH +RUN --mount=type=cache,target=/root/.cache/go-build \ + GOOS=$TARGETOS GOARCH=$TARGETARCH \ + go build -trimpath -ldflags="-s -w -buildid=" -o /out/snoop-api $APP_DIR + +######################## +# 2) Minimal runtime +######################## +FROM gcr.io/distroless/static:nonroot +# Copy CA bundle for HTTPS calls +COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ +COPY --from=build /out/snoop-api /snoop-api +USER nonroot:nonroot +EXPOSE 8080 +ENTRYPOINT ["/snoop-api"] \ No newline at end of file diff --git a/server/cmd/api/main.go b/server/cmd/api/main.go new file mode 100644 index 0000000..696fe6c --- /dev/null +++ b/server/cmd/api/main.go @@ -0,0 +1,96 @@ +package main + +import ( + "log" + "net/http" + "os" + "time" + + "smoop-api/internal/config" + "smoop-api/internal/crypto" + "smoop-api/internal/db" + "smoop-api/internal/models" + "smoop-api/internal/router" + "smoop-api/internal/storage" + + "gorm.io/gorm" +) + +func main() { + // 1) Load config from Vault + // useDev := os.Getenv("CONFIG_MODE") == "dev" + var err error + var cfg *config.Config + // if useDev { + // cfg, err = config.LoadDev() + // } else { + // cfg, err = config.Load() + // } + // if err != nil { + // log.Printf("config: %v", err) + // } + cfg, err = config.LoadDev() + if err != nil { + log.Fatalf("config: %v", err) + } + // 2) DB + gormDB, err := db.Open(cfg.DB.DSN) + if err != nil { + log.Fatalf("db: %v", err) + } + if err := db.AutoMigrate(gormDB); err != nil { + log.Fatalf("migrate: %v", err) + } + + if err := seedAdmin(gormDB); err != nil { + log.Fatalf("seed admin: %v", err) + } + + // 3) MinIO + minioClient, err := storage.NewMinio(cfg.MinIO) + if err != nil { + log.Fatalf("minio: %v", err) + } + if err := storage.EnsureBuckets(minioClient, cfg.MinIO); err != nil { + log.Fatalf("ensure buckets: %v", err) + } + + // 4) Router + engine := router.Build(gormDB, minioClient, cfg) + + srv := &http.Server{ + Addr: ":8080", + Handler: engine, + ReadHeaderTimeout: 10 * time.Second, + } + log.Println("listening on :8080") + log.Fatal(srv.ListenAndServe()) +} + +func seedAdmin(g *gorm.DB) error { + password := os.Getenv("ADMIN_PASSWORD") + if password == "" { + return nil // nothing to do + } + username := os.Getenv("ADMIN_USERNAME") + if username == "" { + username = "admin" + } + var count int64 + if err := g.Model(&models.User{}).Where("username = ?", username).Count(&count).Error; err != nil { + return err + } + if count > 0 { + return nil + } + hash, err := crypto.Hash(password, crypto.DefaultArgon2) + if err != nil { + return err + } + u := models.User{Username: username, Password: hash, Role: models.RoleAdmin} + if err := g.Create(&u).Error; err != nil { + return err + } + log.Printf("created admin user %s", username) + return nil +} diff --git a/server/go.mod b/server/go.mod new file mode 100644 index 0000000..3e4fc2b --- /dev/null +++ b/server/go.mod @@ -0,0 +1,67 @@ +module smoop-api + +go 1.23.0 + +require github.com/gin-gonic/gin v1.10.1 + +require ( + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/go-ini/ini v1.67.0 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/hashicorp/go-cleanhttp v0.5.2 // indirect + github.com/hashicorp/go-retryablehttp v0.7.1 // indirect + github.com/hashicorp/go-rootcerts v1.0.2 // indirect + github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect + github.com/jackc/pgx/v5 v5.6.0 // indirect + github.com/jackc/puddle/v2 v2.2.2 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // indirect + github.com/klauspost/compress v1.18.0 // indirect + github.com/minio/crc64nvme v1.0.2 // indirect + github.com/minio/md5-simd v1.1.2 // indirect + github.com/mitchellh/go-homedir v1.1.0 // indirect + github.com/philhofer/fwd v1.2.0 // indirect + github.com/rs/xid v1.6.0 // indirect + github.com/ryanuber/go-glob v1.0.0 // indirect + github.com/tinylib/msgp v1.3.0 // indirect + golang.org/x/sync v0.15.0 // indirect + golang.org/x/time v0.0.0-20220922220347-f3bd1da661af // indirect +) + +require ( + github.com/bytedance/sonic v1.11.6 // indirect + github.com/bytedance/sonic/loader v0.1.1 // indirect + github.com/cloudwego/base64x v0.1.4 // indirect + github.com/cloudwego/iasm v0.2.0 // indirect + github.com/gabriel-vasile/mimetype v1.4.3 // indirect + github.com/gin-contrib/sse v0.1.0 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.20.0 // indirect + github.com/goccy/go-json v0.10.5 // indirect + github.com/golang-jwt/jwt v3.2.2+incompatible + github.com/golang-jwt/jwt/v5 v5.3.0 + github.com/gorilla/websocket v1.5.3 + github.com/hashicorp/vault-client-go v0.4.3 + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/cpuid/v2 v2.2.11 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/minio/minio-go/v7 v7.0.95 + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/pelletier/go-toml/v2 v2.2.2 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.2.12 // indirect + golang.org/x/arch v0.8.0 // indirect + golang.org/x/crypto v0.39.0 // indirect + golang.org/x/net v0.41.0 // indirect + golang.org/x/sys v0.33.0 // indirect + golang.org/x/text v0.26.0 // indirect + google.golang.org/protobuf v1.34.1 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + gorm.io/driver/postgres v1.6.0 + gorm.io/gorm v1.30.1 +) diff --git a/server/go.sum b/server/go.sum new file mode 100644 index 0000000..20988c2 --- /dev/null +++ b/server/go.sum @@ -0,0 +1,157 @@ +github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0= +github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4= +github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM= +github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= +github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= +github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= +github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= +github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= +github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ= +github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= +github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A= +github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8= +github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= +github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= +github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= +github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= +github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= +github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= +github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= +github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= +github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= +github.com/hashicorp/go-retryablehttp v0.7.1 h1:sUiuQAnLlbvmExtFQs72iFW/HXeUn8Z1aJLQ4LJJbTQ= +github.com/hashicorp/go-retryablehttp v0.7.1/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY= +github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc= +github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= +github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 h1:kes8mmyCpxJsI7FTwtzRqEy9CdjCtrXrXGuOpxEA7Ts= +github.com/hashicorp/go-secure-stdlib/strutil v0.1.2/go.mod h1:Gou2R9+il93BqX25LAKCLuM+y9U2T4hlwvT1yprcna4= +github.com/hashicorp/vault-client-go v0.4.3 h1:zG7STGVgn/VK6rnZc0k8PGbfv2x/sJExRKHSUg3ljWc= +github.com/hashicorp/vault-client-go v0.4.3/go.mod h1:4tDw7Uhq5XOxS1fO+oMtotHL7j4sB9cp0T7U6m4FzDY= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.6.0 h1:SWJzexBzPL5jb0GEsrPMLIsi/3jOo7RHlzTjcAeDrPY= +github.com/jackc/pgx/v5 v5.6.0/go.mod h1:DNZ/vlrUnhWCoFGxHAG8U2ljioxukquj7utPDgtQdTw= +github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= +github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= +github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= +github.com/klauspost/cpuid/v2 v2.2.11 h1:0OwqZRYI2rFrjS4kvkDnqJkKHdHaRnCm68/DY4OxRzU= +github.com/klauspost/cpuid/v2 v2.2.11/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/minio/crc64nvme v1.0.2 h1:6uO1UxGAD+kwqWWp7mBFsi5gAse66C4NXO8cmcVculg= +github.com/minio/crc64nvme v1.0.2/go.mod h1:eVfm2fAzLlxMdUGc0EEBGSMmPwmXD5XiNRpnu9J3bvg= +github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34= +github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM= +github.com/minio/minio-go/v7 v7.0.95 h1:ywOUPg+PebTMTzn9VDsoFJy32ZuARN9zhB+K3IYEvYU= +github.com/minio/minio-go/v7 v7.0.95/go.mod h1:wOOX3uxS334vImCNRVyIDdXX9OsXDm89ToynKgqUKlo= +github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= +github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= +github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM= +github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU= +github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= +github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk= +github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/tinylib/msgp v1.3.0 h1:ULuf7GPooDaIlbyvgAxBV/FI7ynli6LZ1/nVUNu+0ww= +github.com/tinylib/msgp v1.3.0/go.mod h1:ykjzy2wzgrlvpDCRc4LA8UXy6D8bzMSuAF3WD57Gok0= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= +github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc= +golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= +golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= +golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= +golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM= +golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U= +golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= +golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= +golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= +golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8= +golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= +golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= +golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug= +golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4= +golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= +golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= +golang.org/x/time v0.0.0-20220922220347-f3bd1da661af h1:Yx9k8YCG3dvF87UAn2tu2HQLf2dt/eR1bXxpLMWeH+Y= +golang.org/x/time v0.0.0-20220922220347-f3bd1da661af/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= +google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4= +gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo= +gorm.io/gorm v1.30.1 h1:lSHg33jJTBxs2mgJRfRZeLDG+WZaHYCk3Wtfl6Ngzo4= +gorm.io/gorm v1.30.1/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE= +nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/server/internal/config/config.go b/server/internal/config/config.go new file mode 100644 index 0000000..fa0be6c --- /dev/null +++ b/server/internal/config/config.go @@ -0,0 +1,205 @@ +package config + +import ( + "fmt" + "os" + "strconv" + "strings" + "time" + + "smoop-api/internal/vault" +) + +type Config struct { + DB struct { + DSN string + } + MinIO struct { + Endpoint string + AccessKey string + SecretKey string + UseSSL bool + RecordsBucket string + LivestreamBucket string + PresignTTL time.Duration + } + JWTSecret []byte +} + +func Load() (*Config, error) { + addr := os.Getenv("VAULT_ADDR") + token := os.Getenv("VAULT_TOKEN") + + // New style: explicit KV v2 mount + key + mount := os.Getenv("VAULT_KV_MOUNT") // e.g. "kv" or "secret" + key := os.Getenv("VAULT_KV_KEY") // e.g. "snoop" + + // Back-compat: allow legacy VAULT_KV_PATH like "kv/data/snoop" and derive mount+key + if (mount == "" || key == "") && os.Getenv("VAULT_KV_PATH") != "" { + legacy := strings.Trim(os.Getenv("VAULT_KV_PATH"), "/") + parts := strings.Split(legacy, "/") + if len(parts) >= 2 { + mount = parts[0] + key = parts[len(parts)-1] + } + } + + if addr == "" || token == "" || mount == "" || key == "" { + return nil, fmt.Errorf("VAULT_ADDR, VAULT_TOKEN, VAULT_KV_MOUNT and VAULT_KV_KEY must be set (or provide legacy VAULT_KV_PATH)") + } + + raw, err := vault.ReadKVv2(addr, token, mount, key) + if err != nil { + return nil, err + } + + getStr := func(k string) (string, error) { + v, ok := raw[k].(string) + if !ok || v == "" { + return "", fmt.Errorf("missing secret key: %s", k) + } + return v, nil + } + getBool := func(k string) (bool, error) { + v, ok := raw[k] + if !ok { + return false, fmt.Errorf("missing secret key: %s", k) + } + switch t := v.(type) { + case bool: + return t, nil + case string: + if t == "true" || t == "1" { + return true, nil + } + return false, nil + default: + return false, fmt.Errorf("invalid bool for key %s", k) + } + } + + dbDSN, err := getStr("db_dsn") + if err != nil { + return nil, err + } + endpoint, err := getStr("minio_endpoint") + if err != nil { + return nil, err + } + ak, err := getStr("minio_access_key") + if err != nil { + return nil, err + } + sk, err := getStr("minio_secret_key") + if err != nil { + return nil, err + } + useSSL, err := getBool("minio_use_ssl") + if err != nil { + return nil, err + } + jwt, err := getStr("jwt_secret") + if err != nil { + return nil, err + } + + recordsBucket := "records" + if v, ok := raw["minio_records_bucket"].(string); ok && v != "" { + recordsBucket = v + } + liveBucket := "livestream" + if v, ok := raw["minio_livestream_bucket"].(string); ok && v != "" { + liveBucket = v + } + presignTTL := 15 * time.Minute + if v, ok := raw["minio_presign_ttl_seconds"].(string); ok && v != "" { + var sec int + fmt.Sscanf(v, "%d", &sec) + if sec > 0 { + presignTTL = time.Duration(sec) * time.Second + } + } + + cfg := &Config{} + cfg.DB.DSN = dbDSN + cfg.MinIO.Endpoint = endpoint + cfg.MinIO.AccessKey = ak + cfg.MinIO.SecretKey = sk + cfg.MinIO.UseSSL = useSSL + cfg.MinIO.RecordsBucket = recordsBucket + cfg.MinIO.LivestreamBucket = liveBucket + cfg.MinIO.PresignTTL = presignTTL + cfg.JWTSecret = []byte(jwt) + return cfg, nil +} + +func LoadDev() (*Config, error) { + getRequired := func(k string) (string, error) { + v := os.Getenv(k) + if v == "" { + return "", fmt.Errorf("missing required env %s", k) + } + return v, nil + } + getBoolEnv := func(k string, def bool) bool { + v := strings.ToLower(strings.TrimSpace(os.Getenv(k))) + if v == "true" || v == "1" || v == "yes" { + return true + } + if v == "false" || v == "0" || v == "no" { + return false + } + return def + } + getIntEnv := func(k string, def int) int { + if v := strings.TrimSpace(os.Getenv(k)); v != "" { + if n, err := strconv.Atoi(v); err == nil { + return n + } + } + return def + } + dbDSN, err := getRequired("DB_DSN") + if err != nil { + return nil, err + } + endpoint, err := getRequired("MINIO_ENDPOINT") + if err != nil { + return nil, err + } + ak, err := getRequired("MINIO_ACCESS_KEY") + if err != nil { + return nil, err + } + sk, err := getRequired("MINIO_SECRET_KEY") + if err != nil { + return nil, err + } + jwt, err := getRequired("JWT_SECRET") + if err != nil { + return nil, err + } + + useSSL := getBoolEnv("MINIO_USE_SSL", false) + recordsBucket := os.Getenv("MINIO_RECORDS_BUCKET") + if recordsBucket == "" { + recordsBucket = "records" + } + liveBucket := os.Getenv("MINIO_LIVESTREAM_BUCKET") + if liveBucket == "" { + liveBucket = "livestream" + } + presignTTL := time.Duration(getIntEnv("MINIO_PRESIGN_TTL_SECONDS", 900)) * time.Second + + cfg := &Config{} + cfg.DB.DSN = dbDSN + cfg.MinIO.Endpoint = endpoint + cfg.MinIO.AccessKey = ak + cfg.MinIO.SecretKey = sk + cfg.MinIO.UseSSL = useSSL + cfg.MinIO.RecordsBucket = recordsBucket + cfg.MinIO.LivestreamBucket = liveBucket + cfg.MinIO.PresignTTL = presignTTL + cfg.JWTSecret = []byte(jwt) + return cfg, nil +} diff --git a/server/internal/crypto/argon2id.go b/server/internal/crypto/argon2id.go new file mode 100644 index 0000000..5bcb16e --- /dev/null +++ b/server/internal/crypto/argon2id.go @@ -0,0 +1,102 @@ +package crypto + +import ( + "crypto/rand" + "crypto/subtle" + "encoding/base64" + "errors" + "fmt" + "strconv" + "strings" + + "golang.org/x/crypto/argon2" +) + +type Argon2Params struct { + Memory uint32 // KiB + Time uint32 + Threads uint8 + SaltLen uint32 + KeyLen uint32 +} + +var DefaultArgon2 = Argon2Params{ + Memory: 7168, // 7 MiB + Time: 5, + Threads: 1, + SaltLen: 16, + KeyLen: 32, +} + +// Hash returns PHC string: $argon2id$v=19$m=...,t=...,p=...$$ +func Hash(password string, p Argon2Params) (string, error) { + salt := make([]byte, p.SaltLen) + if _, err := rand.Read(salt); err != nil { + return "", err + } + sum := argon2.IDKey([]byte(password), salt, p.Time, p.Memory, p.Threads, p.KeyLen) + return fmt.Sprintf("$argon2id$v=19$m=%d,t=%d,p=%d$%s$%s", + p.Memory, p.Time, p.Threads, + base64.RawStdEncoding.EncodeToString(salt), + base64.RawStdEncoding.EncodeToString(sum), + ), nil +} + +func Verify(password, phc string) (bool, error) { + parts := strings.Split(phc, "$") + // Expect: ["", "argon2id", "v=19", "m=...,t=...,p=...", "", ""] + if len(parts) != 6 || parts[1] != "argon2id" || parts[2] != "v=19" { + return false, errors.New("invalid PHC header") + } + + // parse params + var mem, iters uint64 + var threads uint64 + for _, kv := range strings.Split(parts[3], ",") { + if strings.HasPrefix(kv, "m=") { + v := strings.TrimPrefix(kv, "m=") + x, err := strconv.ParseUint(v, 10, 32) + if err != nil { + return false, fmt.Errorf("mem: %w", err) + } + mem = x + } else if strings.HasPrefix(kv, "t=") { + v := strings.TrimPrefix(kv, "t=") + x, err := strconv.ParseUint(v, 10, 32) + if err != nil { + return false, fmt.Errorf("time: %w", err) + } + iters = x + } else if strings.HasPrefix(kv, "p=") { + v := strings.TrimPrefix(kv, "p=") + x, err := strconv.ParseUint(v, 10, 8) + if err != nil { + return false, fmt.Errorf("threads: %w", err) + } + threads = x + } + } + if mem == 0 || iters == 0 || threads == 0 { + return false, errors.New("invalid PHC params") + } + + salt, err := b64DecodeFlex(parts[4]) + if err != nil { + return false, fmt.Errorf("salt decode: %w", err) + } + + want, err := b64DecodeFlex(parts[5]) + if err != nil { + return false, fmt.Errorf("hash decode: %w", err) + } + + got := argon2.IDKey([]byte(password), salt, uint32(iters), uint32(mem), uint8(threads), uint32(len(want))) + return subtle.ConstantTimeCompare(got, want) == 1, nil +} + +func b64DecodeFlex(s string) ([]byte, error) { + if b, err := base64.RawStdEncoding.DecodeString(s); err == nil { + return b, nil + } + return base64.StdEncoding.DecodeString(s) // padded fallback +} diff --git a/server/internal/crypto/jwt.go b/server/internal/crypto/jwt.go new file mode 100644 index 0000000..04e822b --- /dev/null +++ b/server/internal/crypto/jwt.go @@ -0,0 +1,30 @@ +package crypto + +import ( + "time" + + "github.com/golang-jwt/jwt/v5" +) + +type JWTManager struct { + secret []byte +} + +func NewJWT(secret []byte) *JWTManager { + return &JWTManager{secret: secret} +} + +func (j *JWTManager) Generate(userID uint, username, role string) (string, error) { + t := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ + "sub": userID, + "name": username, + "role": role, + "iat": time.Now().Unix(), + "exp": time.Now().Add(24 * time.Hour).Unix(), + }) + return t.SignedString(j.secret) +} + +func (j *JWTManager) Parse(tok string) (*jwt.Token, error) { + return jwt.Parse(tok, func(t *jwt.Token) (interface{}, error) { return j.secret, nil }) +} diff --git a/server/internal/db/db.go b/server/internal/db/db.go new file mode 100644 index 0000000..5204c51 --- /dev/null +++ b/server/internal/db/db.go @@ -0,0 +1,20 @@ +package db + +import ( + "gorm.io/driver/postgres" + "gorm.io/gorm" + + "smoop-api/internal/models" +) + +func Open(dsn string) (*gorm.DB, error) { + return gorm.Open(postgres.Open(dsn), &gorm.Config{}) +} + +func AutoMigrate(db *gorm.DB) error { + return db.AutoMigrate( + &models.User{}, + &models.Device{}, + &models.Record{}, + ) +} diff --git a/server/internal/dto/auth.go b/server/internal/dto/auth.go new file mode 100644 index 0000000..4fbe3e4 --- /dev/null +++ b/server/internal/dto/auth.go @@ -0,0 +1,20 @@ +package dto + +type AuthDto struct { + Username string `json:"username" binding:"required"` + Password string `json:"password" binding:"required"` +} + +type AccessTokenDto struct { + AccessToken string `json:"accessToken"` +} + +type ChangePasswordDto struct { + UserID uint `json:"userId,omitempty"` + OldPassword string `json:"oldPassword"` + NewPassword string `json:"newPassword" binding:"required"` +} + +type CheckTokenResultDto struct { + IsValid bool `json:"isValid"` +} diff --git a/server/internal/dto/device.go b/server/internal/dto/device.go new file mode 100644 index 0000000..ce77fa4 --- /dev/null +++ b/server/internal/dto/device.go @@ -0,0 +1,56 @@ +package dto + +import "smoop-api/internal/models" + +type DeviceDto struct { + GUID string `json:"guid"` + Name string `json:"name"` + Users []UserDto `json:"users,omitempty"` +} + +type DeviceListDto struct { + Devices []DeviceDto `json:"devices"` + Offset int `json:"offset"` + Limit int `json:"limit"` + Total int64 `json:"total"` +} + +type CreateDeviceDto struct { + GUID string `json:"guid" binding:"required"` + Name string `json:"name" binding:"required"` + UserIDs []uint `json:"userIds"` +} + +type RenameDeviceDto struct { + Name string `json:"name" binding:"required"` +} + +type EditDeviceToUserRelationDto struct { + UserID uint `json:"userId"` + UserIDs []uint `json:"userIds"` +} + +// Remove users: accept one or many (kept separate for clarity of intent) +type RemoveDeviceUsersDto struct { + UserID uint `json:"userId"` + UserIDs []uint `json:"userIds"` +} + +// Replace users: set entire list (may be empty to clear all) +type SetDeviceUsersDto struct { + UserIDs []uint `json:"userIds"` +} + +func MapDevice(d models.Device) DeviceDto { + out := DeviceDto{ + GUID: d.GUID, + Name: d.Name, + } + if len(d.Users) > 0 { + out.Users = make([]UserDto, 0, len(d.Users)) + for _, u := range d.Users { + out.Users = append(out.Users, MapUser(u)) + } + } + return out +} diff --git a/server/internal/dto/record.go b/server/internal/dto/record.go new file mode 100644 index 0000000..04b4f9e --- /dev/null +++ b/server/internal/dto/record.go @@ -0,0 +1,14 @@ +package dto + +type RecordDto struct { + ID uint `json:"id"` + StartedAt int64 `json:"startedAt"` + StoppedAt int64 `json:"stoppedAt"` +} + +type RecordListDto struct { + Records []RecordDto `json:"records"` + Offset int `json:"offset"` + Limit int `json:"limit"` + Total int64 `json:"total"` +} diff --git a/server/internal/dto/user.go b/server/internal/dto/user.go new file mode 100644 index 0000000..a140e44 --- /dev/null +++ b/server/internal/dto/user.go @@ -0,0 +1,29 @@ +package dto + +import "smoop-api/internal/models" + +type UserDto struct { + ID uint `json:"id"` + Username string `json:"username"` + Role models.Role `json:"role"` +} + +type UserRoleDto struct { + Role models.Role `json:"role" binding:"required,oneof=admin user"` +} + +// CreateUserDto is used by POST /users/create to create a new user with a role. +// Role must be one of: "admin" | "user". +type CreateUserDto struct { + Username string `json:"username" binding:"required"` + Password string `json:"password" binding:"required"` + Role string `json:"role" binding:"required,oneof=admin user"` +} + +func MapUser(u models.User) UserDto { + return UserDto{ + ID: u.ID, + Username: u.Username, + Role: u.Role, + } +} diff --git a/server/internal/handlers/auth.go b/server/internal/handlers/auth.go new file mode 100644 index 0000000..22b4067 --- /dev/null +++ b/server/internal/handlers/auth.go @@ -0,0 +1,117 @@ +package handlers + +import ( + "log" + "net/http" + + "github.com/gin-gonic/gin" + "gorm.io/gorm" + + "smoop-api/internal/crypto" + "smoop-api/internal/dto" + "smoop-api/internal/models" +) + +type AuthHandler struct { + db *gorm.DB + jwtMgr *crypto.JWTManager +} + +func NewAuthHandler(db *gorm.DB, jwt *crypto.JWTManager) *AuthHandler { + return &AuthHandler{db: db, jwtMgr: jwt} +} + +func (h *AuthHandler) SignUp(c *gin.Context) { + var req dto.AuthDto + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + hash, err := crypto.Hash(req.Password, crypto.DefaultArgon2) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "hash failed"}) + return + } + u := models.User{Username: req.Username, Password: hash, Role: models.RoleUser} + if err := h.db.Create(&u).Error; err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "username exists"}) + return + } + tok, _ := h.jwtMgr.Generate(u.ID, u.Username, string(u.Role)) + c.JSON(http.StatusCreated, dto.AccessTokenDto{AccessToken: tok}) +} + +func (h *AuthHandler) SignIn(c *gin.Context) { + var req dto.AuthDto + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + var u models.User + if err := h.db.Where("username = ?", req.Username).First(&u).Error; err != nil { + c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid credentials - login"}) + return + } + ok, verr := crypto.Verify(req.Password, u.Password) + if verr != nil { + log.Printf("verify error: %v", verr) // keep log-only in prod + } + if !ok { + c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid credentials - password"}) + return + } + tok, _ := h.jwtMgr.Generate(u.ID, u.Username, string(u.Role)) + c.JSON(http.StatusCreated, dto.AccessTokenDto{AccessToken: tok}) +} + +func (h *AuthHandler) ChangePassword(c *gin.Context) { + var req dto.ChangePasswordDto + if err := c.ShouldBindJSON(&req); err != nil || req.NewPassword == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "bad request"}) + return + } + claims := MustClaims(c) + currentUID := ClaimUserID(claims) + isAdmin := ClaimRole(claims) == "admin" + + targetID := currentUID + if req.UserID != 0 { + if !isAdmin && req.UserID != currentUID { + c.JSON(http.StatusForbidden, gin.H{"error": "forbidden"}) + return + } + targetID = req.UserID + } + + var u models.User + if err := h.db.First(&u, targetID).Error; err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": "user not found"}) + return + } + + if !isAdmin { + ok, _ := crypto.Verify(req.OldPassword, u.Password) + if !ok { + c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid old password"}) + return + } + } + + hash, _ := crypto.Hash(req.NewPassword, crypto.DefaultArgon2) + u.Password = hash + if err := h.db.Save(&u).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "update failed"}) + return + } + c.Status(http.StatusCreated) +} + +func (h *AuthHandler) CheckToken(c *gin.Context) { + var req dto.AccessTokenDto + if err := c.ShouldBindJSON(&req); err != nil || req.AccessToken == "" { + c.JSON(http.StatusBadRequest, gin.H{"isValid": false}) + return + } + _, err := h.jwtMgr.Parse(req.AccessToken) + c.JSON(http.StatusCreated, gin.H{"isValid": err == nil}) +} diff --git a/server/internal/handlers/devices.go b/server/internal/handlers/devices.go new file mode 100644 index 0000000..da95db0 --- /dev/null +++ b/server/internal/handlers/devices.go @@ -0,0 +1,250 @@ +package handlers + +import ( + "fmt" + "net/http" + "strconv" + "strings" + + "github.com/gin-gonic/gin" + "gorm.io/gorm" + + "smoop-api/internal/dto" + "smoop-api/internal/models" +) + +type DevicesHandler struct { + db *gorm.DB +} + +func NewDevicesHandler(db *gorm.DB) *DevicesHandler { return &DevicesHandler{db: db} } + +func (h *DevicesHandler) List(c *gin.Context) { + offset, _ := strconv.Atoi(c.DefaultQuery("offset", "0")) + limit, _ := strconv.Atoi(c.DefaultQuery("limit", "50")) + if limit <= 0 || limit > 200 { + limit = 50 + } + + var total int64 + h.db.Model(&models.Device{}).Count(&total) + + var devs []models.Device + if err := h.db.Preload("Users").Offset(offset).Limit(limit).Find(&devs).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "query failed"}) + return + } + + out := make([]dto.DeviceDto, 0, len(devs)) + for _, d := range devs { + out = append(out, dto.MapDevice(d)) + } + c.JSON(http.StatusOK, dto.DeviceListDto{Devices: out, Offset: offset, Limit: limit, Total: total}) +} + +func (h *DevicesHandler) Create(c *gin.Context) { + var req dto.CreateDeviceDto + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + d := models.Device{GUID: req.GUID, Name: req.Name} + if err := h.db.Create(&d).Error; err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "device exists?"}) + return + } + // Optional initial user assignments + if len(req.UserIDs) > 0 { + users, err := h.fetchUsers(req.UserIDs) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + if err := h.db.Model(&d).Association("Users").Append(&users); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "link failed"}) + return + } + } + // Return with users + var withUsers models.Device + if err := h.db.Preload("Users").Where("guid = ?", d.GUID).First(&withUsers).Error; err != nil { + c.JSON(http.StatusCreated, dto.DeviceDto{GUID: d.GUID, Name: d.Name}) + return + } + c.JSON(http.StatusCreated, dto.MapDevice(withUsers)) +} + +func (h *DevicesHandler) Rename(c *gin.Context) { + guid := c.Param("guid") + var d models.Device + if err := h.db.Where("guid = ?", guid).First(&d).Error; err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": "device not found"}) + return + } + var req dto.RenameDeviceDto + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + d.Name = req.Name + if err := h.db.Save(&d).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "save failed"}) + return + } + c.JSON(http.StatusCreated, dto.DeviceDto{GUID: d.GUID, Name: d.Name}) +} + +func (h *DevicesHandler) AddToUser(c *gin.Context) { + guid := c.Param("guid") + var d models.Device + if err := h.db.Where("guid = ?", guid).First(&d).Error; err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": "device not found"}) + return + } + var req dto.EditDeviceToUserRelationDto + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + ids := req.UserIDs + if req.UserID != 0 { + ids = append(ids, req.UserID) + } + if len(ids) == 0 { + c.JSON(http.StatusBadRequest, gin.H{"error": "userIds or userId required"}) + return + } + + users, err := h.fetchUsers(ids) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + if err := h.db.Model(&d).Association("Users").Append(&users); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "link failed"}) + return + } + var withUsers models.Device + _ = h.db.Preload("Users").Where("guid = ?", d.GUID).First(&withUsers).Error + c.JSON(http.StatusCreated, dto.MapDevice(withUsers)) +} + +// SetUsers replaces the users of a device with the provided list. +// Passing an empty list clears all assignments (covers the "no user assigned" case). +func (h *DevicesHandler) SetUsers(c *gin.Context) { + guid := c.Param("guid") + var d models.Device + if err := h.db.Where("guid = ?", guid).First(&d).Error; err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": "device not found"}) + return + } + var req dto.SetDeviceUsersDto + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + // Load users (if any) + users := []models.User{} + if len(req.UserIDs) > 0 { + found, err := h.fetchUsers(req.UserIDs) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + users = found + } + // Replace association: Clear() then Append(new) + if err := h.db.Model(&d).Association("Users").Clear(); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "clear failed"}) + return + } + if len(users) > 0 { + if err := h.db.Model(&d).Association("Users").Append(&users); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "link failed"}) + return + } + } + var withUsers models.Device + _ = h.db.Preload("Users").Where("guid = ?", d.GUID).First(&withUsers).Error + c.JSON(http.StatusCreated, dto.MapDevice(withUsers)) +} + +func (h *DevicesHandler) RemoveFromUser(c *gin.Context) { + guid := c.Param("guid") + var d models.Device + if err := h.db.Where("guid = ?", guid).First(&d).Error; err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": "device not found"}) + return + } + var req dto.RemoveDeviceUsersDto + _ = c.ShouldBindJSON(&req) // ignore error; we support query fallback + + ids := make([]uint, 0, len(req.UserIDs)+1) + if req.UserID != 0 { + ids = append(ids, req.UserID) + } + if len(req.UserIDs) > 0 { + ids = append(ids, req.UserIDs...) + } + // query fallback + if len(ids) == 0 { + if q := strings.TrimSpace(c.Query("userId")); q != "" { + if n, err := strconv.Atoi(q); err == nil && n > 0 { + ids = append(ids, uint(n)) + } + } + if q := strings.TrimSpace(c.Query("userIds")); q != "" { + for _, p := range strings.Split(q, ",") { + p = strings.TrimSpace(p) + if n, err := strconv.Atoi(p); err == nil && n > 0 { + ids = append(ids, uint(n)) + } + } + } + } + if len(ids) == 0 { + c.JSON(http.StatusBadRequest, gin.H{"error": "userIds or userId required"}) + return + } + + users, err := h.fetchUsers(ids) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + if len(users) == 0 { + c.JSON(http.StatusOK, dto.DeviceDto{GUID: d.GUID, Name: d.Name}) + return + } + if err := h.db.Model(&d).Association("Users").Delete(&users); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "unlink failed"}) + return + } + var withUsers models.Device + _ = h.db.Preload("Users").Where("guid = ?", d.GUID).First(&withUsers).Error + c.JSON(http.StatusOK, dto.MapDevice(withUsers)) +} + +func (h *DevicesHandler) fetchUsers(ids []uint) ([]models.User, error) { + unique := make(map[uint]struct{}, len(ids)) + clean := make([]uint, 0, len(ids)) + for _, id := range ids { + if id != 0 { + if _, ok := unique[id]; !ok { + unique[id] = struct{}{} + clean = append(clean, id) + } + } + } + if len(clean) == 0 { + return nil, nil + } + var users []models.User + if err := h.db.Find(&users, clean).Error; err != nil { + return nil, err + } + if len(users) != len(clean) { + return nil, fmt.Errorf("some users not found") + } + return users, nil +} diff --git a/server/internal/handlers/helpers.go b/server/internal/handlers/helpers.go new file mode 100644 index 0000000..7ae9e13 --- /dev/null +++ b/server/internal/handlers/helpers.go @@ -0,0 +1,78 @@ +package handlers + +import ( + "net/http" + "smoop-api/internal/crypto" + "strings" + + "github.com/gin-gonic/gin" + "github.com/golang-jwt/jwt/v5" +) + +func Auth(jwtMgr *crypto.JWTManager) gin.HandlerFunc { + return func(c *gin.Context) { + h := c.GetHeader("Authorization") + if !strings.HasPrefix(h, "Bearer ") { + c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "missing bearer token"}) + return + } + tok := strings.TrimPrefix(h, "Bearer ") + token, err := jwtMgr.Parse(tok) + if err != nil || !token.Valid { + c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "invalid token"}) + return + } + claims, _ := token.Claims.(jwt.MapClaims) + c.Set("claims", claims) + c.Next() + } +} + +func RequireRole(role string) gin.HandlerFunc { + return func(c *gin.Context) { + claims := MustClaims(c) + if ClaimRole(claims) != role { + c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"error": "forbidden"}) + return + } + c.Next() + } +} + +// helpers used by handlers +func MustClaims(c *gin.Context) map[string]interface{} { + val, ok := c.Get("claims") + if !ok { + return jwt.MapClaims{} + } + switch t := val.(type) { + case jwt.MapClaims: + return t + case map[string]interface{}: + return jwt.MapClaims(t) + default: + return jwt.MapClaims{} + } +} +func ClaimUserID(claims map[string]interface{}) uint { + if claims == nil { + return 0 + } + if v, ok := claims["sub"]; ok { + switch n := v.(type) { + case float64: + return uint(n) + case int: + return uint(n) + case int64: + return uint(n) + } + } + return 0 +} +func ClaimRole(claims map[string]interface{}) string { + if r, ok := claims["role"].(string); ok { + return r + } + return "" +} diff --git a/server/internal/handlers/livestream.go b/server/internal/handlers/livestream.go new file mode 100644 index 0000000..ada050b --- /dev/null +++ b/server/internal/handlers/livestream.go @@ -0,0 +1,211 @@ +package handlers + +import ( + "bytes" + "context" + "fmt" + "io" + "net/http" + "sync" + "time" + + "github.com/gin-gonic/gin" + "github.com/gorilla/websocket" + "github.com/minio/minio-go/v7" +) + +// --- Minimal in-file hub (per-GUID), fanout to viewers and upload to MinIO --- + +type stream struct { + GUID string + writer io.WriteCloser + active bool + object string + mu sync.RWMutex + view map[*viewer]struct{} +} + +// Close implements io.WriteCloser. +func (s *stream) Close() error { + panic("unimplemented") +} + +func (s *stream) Write(p []byte) (int, error) { + s.mu.RLock() + defer s.mu.RUnlock() + if !s.active { + return 0, fmt.Errorf("not active") + } + for v := range s.view { + select { + case v.out <- p: + default: + } + } + return s.writer.Write(p) +} + +type viewer struct{ out chan []byte } + +type liveHub struct { + mu sync.RWMutex + streams map[string]*stream + minio *minio.Client + bucket string +} + +func newLiveHub(mc *minio.Client, bucket string) *liveHub { + return &liveHub{streams: map[string]*stream{}, minio: mc, bucket: bucket} +} + +func (h *liveHub) start(guid string) (io.WriteCloser, string, error) { + h.mu.Lock() + defer h.mu.Unlock() + + s, ok := h.streams[guid] + if !ok { + s = &stream{GUID: guid, view: map[*viewer]struct{}{}} + h.streams[guid] = s + } + if s.active { + return nil, "", fmt.Errorf("already active") + } + key := fmt.Sprintf("%s/live_%d.raw", guid, time.Now().Unix()) + + pr, pw := io.Pipe() + s.writer = pw + s.object = key + s.active = true + + go func(reader io.Reader, bucket, object string) { + // naive buffering for simplicity + buf := new(bytes.Buffer) + _, _ = io.Copy(buf, reader) + _, _ = h.minio.PutObject(context.Background(), bucket, object, bytes.NewReader(buf.Bytes()), int64(buf.Len()), minio.PutObjectOptions{ + ContentType: "application/octet-stream", + }) + }(pr, h.bucket, key) + + return s, key, nil +} + +func (h *liveHub) stop(guid string) (string, error) { + h.mu.Lock() + defer h.mu.Unlock() + + s, ok := h.streams[guid] + if !ok || !s.active { + return "", fmt.Errorf("not active") + } + _ = s.writer.Close() + s.active = false + return s.object, nil +} + +func (h *liveHub) join(guid string) *viewer { + h.mu.Lock() + defer h.mu.Unlock() + + s, ok := h.streams[guid] + if !ok { + s = &stream{GUID: guid, view: map[*viewer]struct{}{}} + h.streams[guid] = s + } + v := &viewer{out: make(chan []byte, 64)} + s.view[v] = struct{}{} + return v +} + +func (h *liveHub) leave(guid string, v *viewer) { + h.mu.Lock() + defer h.mu.Unlock() + + if s, ok := h.streams[guid]; ok { + delete(s.view, v) + close(v.out) + } +} + +// --- Gin handler --- + +type LivestreamHandler struct { + hub *liveHub +} + +func NewLivestreamHandler(minio *minio.Client, bucket string) *LivestreamHandler { + return &LivestreamHandler{hub: newLiveHub(minio, bucket)} +} + +var upgrader = websocket.Upgrader{CheckOrigin: func(r *http.Request) bool { return true }} + +type wsMsg struct { + Type string `json:"type"` // "start", "stop" + Role string `json:"role"` // "device" or "viewer" + GUID string `json:"guid"` // required + Format string `json:"format"` // optional +} + +func (h *LivestreamHandler) Upgrade(c *gin.Context) { + conn, err := upgrader.Upgrade(c.Writer, c.Request, nil) + if err != nil { + return + } + defer conn.Close() + + for { + var m wsMsg + if err := conn.ReadJSON(&m); err != nil { + return + } + if m.GUID == "" { + _ = conn.WriteJSON(gin.H{"error": "guid required"}) + continue + } + switch m.Role { + case "device": + switch m.Type { + case "start": + w, object, err := h.hub.start(m.GUID) + if err != nil { + _ = conn.WriteJSON(gin.H{"error": err.Error()}) + continue + } + _ = conn.WriteJSON(gin.H{"ok": true, "object": object}) + // read binary frames until stop/close + for { + mt, data, err := conn.ReadMessage() + if err != nil { + h.hub.stop(m.GUID) + return + } + if mt == websocket.BinaryMessage { + _, _ = w.Write(data) + } else if mt == websocket.TextMessage { + // best-effort: if a text control announcing stop arrives + var ctrl wsMsg + if err := conn.ReadJSON(&ctrl); err == nil && ctrl.Type == "stop" { + h.hub.stop(m.GUID) + return + } + } + } + case "stop": + _, _ = h.hub.stop(m.GUID) + _ = conn.WriteJSON(gin.H{"ok": true}) + default: + _ = conn.WriteJSON(gin.H{"error": "unknown type"}) + } + case "viewer": + v := h.hub.join(m.GUID) + defer h.hub.leave(m.GUID, v) + _ = conn.WriteJSON(gin.H{"ok": true}) + for frame := range v.out { + if err := conn.WriteMessage(websocket.BinaryMessage, frame); err != nil { + return + } + } + default: + _ = conn.WriteJSON(gin.H{"error": "unknown role"}) + } + } +} diff --git a/server/internal/handlers/records.go b/server/internal/handlers/records.go new file mode 100644 index 0000000..6110f64 --- /dev/null +++ b/server/internal/handlers/records.go @@ -0,0 +1,123 @@ +package handlers + +import ( + "context" + "fmt" + "mime/multipart" + "net/http" + "path" + "strconv" + "time" + + "github.com/gin-gonic/gin" + "github.com/minio/minio-go/v7" + "gorm.io/gorm" + + "smoop-api/internal/dto" + "smoop-api/internal/models" +) + +type RecordsHandler struct { + db *gorm.DB + minio *minio.Client + recordsBucket string + presignTTL time.Duration +} + +func NewRecordsHandler(db *gorm.DB, mc *minio.Client, bucket string, ttl time.Duration) *RecordsHandler { + return &RecordsHandler{db: db, minio: mc, recordsBucket: bucket, presignTTL: ttl} +} + +func (h *RecordsHandler) Upload(c *gin.Context) { + file, err := c.FormFile("file") + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "file required"}) + return + } + guid := c.PostForm("guid") + if guid == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "guid required"}) + return + } + startedAt, _ := strconv.ParseInt(c.PostForm("startedAt"), 10, 64) + stoppedAt, _ := strconv.ParseInt(c.PostForm("stoppedAt"), 10, 64) + + var dev models.Device + if err := h.db.Where("guid = ?", guid).First(&dev).Error; err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "device not found"}) + return + } + + objKey := fmt.Sprintf("%s/%d_%s", guid, time.Now().UnixNano(), path.Base(file.Filename)) + if err := h.putFile(c, file, h.recordsBucket, objKey); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "upload failed"}) + return + } + + rec := models.Record{ + DeviceGUID: dev.GUID, + StartedAt: startedAt, + StoppedAt: stoppedAt, + ObjectKey: objKey, + } + if err := h.db.Create(&rec).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "db save failed"}) + return + } + c.JSON(http.StatusCreated, dto.RecordDto{ID: rec.ID, StartedAt: rec.StartedAt, StoppedAt: rec.StoppedAt}) +} + +func (h *RecordsHandler) putFile(c *gin.Context, fh *multipart.FileHeader, bucket, object string) error { + src, err := fh.Open() + if err != nil { + return err + } + defer src.Close() + _, err = h.minio.PutObject(c, bucket, object, src, fh.Size, minio.PutObjectOptions{ + ContentType: fh.Header.Get("Content-Type"), + ContentDisposition: "attachment; filename=" + fh.Filename, + }) + return err +} + +func (h *RecordsHandler) List(c *gin.Context) { + guid := c.Query("guid") + if guid == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "guid is required"}) + return + } + offset, _ := strconv.Atoi(c.DefaultQuery("offset", "0")) + limit, _ := strconv.Atoi(c.DefaultQuery("limit", "50")) + if limit <= 0 || limit > 200 { + limit = 50 + } + + var total int64 + h.db.Model(&models.Record{}).Where("device_guid = ?", guid).Count(&total) + + var recs []models.Record + if err := h.db.Where("device_guid = ?", guid).Order("id desc").Offset(offset).Limit(limit).Find(&recs).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "query failed"}) + return + } + out := make([]dto.RecordDto, 0, len(recs)) + for _, r := range recs { + out = append(out, dto.RecordDto{ID: r.ID, StartedAt: r.StartedAt, StoppedAt: r.StoppedAt}) + } + c.JSON(http.StatusOK, dto.RecordListDto{Records: out, Offset: offset, Limit: limit, Total: total}) +} + +func (h *RecordsHandler) File(c *gin.Context) { + id, _ := strconv.Atoi(c.Param("id")) + var rec models.Record + if err := h.db.First(&rec, id).Error; err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": "not found"}) + return + } + u, err := h.minio.PresignedGetObject(context.Background(), h.recordsBucket, rec.ObjectKey, h.presignTTL, nil) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "presign failed"}) + return + } + c.Redirect(http.StatusFound, u.String()) +} diff --git a/server/internal/handlers/users.go b/server/internal/handlers/users.go new file mode 100644 index 0000000..bfb7b92 --- /dev/null +++ b/server/internal/handlers/users.go @@ -0,0 +1,98 @@ +package handlers + +import ( + "net/http" + "strconv" + "strings" + + "github.com/gin-gonic/gin" + "gorm.io/gorm" + + "smoop-api/internal/crypto" + "smoop-api/internal/dto" + "smoop-api/internal/models" +) + +type UsersHandler struct { + db *gorm.DB +} + +func NewUsersHandler(db *gorm.DB) *UsersHandler { return &UsersHandler{db: db} } + +func (h *UsersHandler) Profile(c *gin.Context) { + claims := MustClaims(c) + uid := ClaimUserID(claims) + + var u models.User + if err := h.db.First(&u, uid).Error; err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": "user not found"}) + return + } + c.JSON(http.StatusOK, dto.MapUser(u)) +} + +func (h *UsersHandler) SetRole(c *gin.Context) { + idStr := c.Param("id") + uid, _ := strconv.Atoi(idStr) + + var u models.User + if err := h.db.First(&u, uid).Error; err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": "user not found"}) + return + } + var req dto.UserRoleDto + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + u.Role = req.Role + if err := h.db.Save(&u).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "save failed"}) + return + } + c.JSON(http.StatusCreated, dto.MapUser(u)) +} + +func (h *UsersHandler) List(c *gin.Context) { + var users []models.User + if err := h.db.Order("id asc").Find(&users).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "query failed"}) + return + } + out := make([]dto.UserDto, 0, len(users)) + for _, u := range users { + out = append(out, dto.MapUser(u)) + } + c.JSON(http.StatusOK, out) +} + +// POST /users/create (admin) — create user with given role +func (h *UsersHandler) Create(c *gin.Context) { + var req dto.CreateUserDto + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + role := models.Role(strings.ToLower(req.Role)) + if role != models.RoleAdmin && role != models.RoleUser { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid role"}) + return + } + hash, err := crypto.Hash(req.Password, crypto.DefaultArgon2) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "hash error"}) + return + } + u := models.User{Username: req.Username, Password: hash, Role: role} + if err := h.db.Create(&u).Error; err != nil { + // hint duplicate username + e := strings.ToLower(err.Error()) + if strings.Contains(e, "duplicate") || strings.Contains(e, "unique") || strings.Contains(e, "exists") { + c.JSON(http.StatusBadRequest, gin.H{"error": "username already exists"}) + return + } + c.JSON(http.StatusInternalServerError, gin.H{"error": "create failed"}) + return + } + c.JSON(http.StatusCreated, dto.MapUser(u)) +} diff --git a/server/internal/models/device.go b/server/internal/models/device.go new file mode 100644 index 0000000..7f12013 --- /dev/null +++ b/server/internal/models/device.go @@ -0,0 +1,12 @@ +package models + +import "time" + +type Device struct { + GUID string `gorm:"primaryKey"` + Name string `gorm:"size:255;not null"` + Users []User `gorm:"many2many:user_devices;joinForeignKey:ID;joinReferences:GUID"` + Records []Record `gorm:"foreignKey:DeviceGUID;references:GUID;constraint:OnDelete:CASCADE"` + CreatedAt time.Time + UpdatedAt time.Time +} diff --git a/server/internal/models/record.go b/server/internal/models/record.go new file mode 100644 index 0000000..ebe7c40 --- /dev/null +++ b/server/internal/models/record.go @@ -0,0 +1,13 @@ +package models + +import "time" + +type Record struct { + ID uint `gorm:"primaryKey"` + DeviceGUID string `gorm:"index;size:64;not null"` + StartedAt int64 `gorm:"not null"` + StoppedAt int64 `gorm:"not null"` + ObjectKey string `gorm:"size:512;not null"` + CreatedAt time.Time + UpdatedAt time.Time +} diff --git a/server/internal/models/user.go b/server/internal/models/user.go new file mode 100644 index 0000000..ea79792 --- /dev/null +++ b/server/internal/models/user.go @@ -0,0 +1,20 @@ +package models + +import "time" + +type Role string + +const ( + RoleAdmin Role = "admin" + RoleUser Role = "user" +) + +type User struct { + ID uint `gorm:"primaryKey"` + Username string `gorm:"uniqueIndex;size:255;not null"` + Password string `gorm:"not null"` + Role Role `gorm:"type:varchar(16);not null;default:'user'"` + Devices []Device `gorm:"many2many:user_devices;joinForeignKey:ID;joinReferences:GUID"` + CreatedAt time.Time + UpdatedAt time.Time +} diff --git a/server/internal/router/router.go b/server/internal/router/router.go new file mode 100644 index 0000000..99c2702 --- /dev/null +++ b/server/internal/router/router.go @@ -0,0 +1,67 @@ +package router + +import ( + "net/http" + "time" + + "github.com/gin-gonic/gin" + "github.com/minio/minio-go/v7" + "gorm.io/gorm" + + "smoop-api/internal/config" + "smoop-api/internal/crypto" + "smoop-api/internal/handlers" +) + +func Build(db *gorm.DB, minio *minio.Client, cfg *config.Config) *gin.Engine { + r := gin.Default() + + jwtMgr := crypto.NewJWT(cfg.JWTSecret) + + // --- Handlers + authH := handlers.NewAuthHandler(db, jwtMgr) + usersH := handlers.NewUsersHandler(db) + devH := handlers.NewDevicesHandler(db) + recH := handlers.NewRecordsHandler(db, minio, cfg.MinIO.RecordsBucket, cfg.MinIO.PresignTTL) + liveH := handlers.NewLivestreamHandler(minio, cfg.MinIO.LivestreamBucket) + + // --- Public auth + r.POST("/auth/signup", authH.SignUp) + r.POST("/auth/signin", authH.SignIn) + r.POST("/auth/check_token", authH.CheckToken) + + // Protected + authMW := handlers.Auth(jwtMgr) + adminOnly := handlers.RequireRole("admin") + + r.POST("/auth/change_password", authMW, authH.ChangePassword) + + r.GET("/users/profile", authMW, usersH.Profile) + r.POST("/users/:id/set_role", authMW, adminOnly, usersH.SetRole) + r.GET("/users", authMW, adminOnly, usersH.List) + r.POST("/users/create", authMW, adminOnly, usersH.Create) + + r.GET("/devices", authMW, devH.List) + r.POST("/devices/create", authMW, devH.Create) + r.POST("/devices/:guid/rename", authMW, devH.Rename) + r.POST("/devices/:guid/add_to_user", authMW, devH.AddToUser) + r.POST("/devices/:guid/set_users", authMW, adminOnly, devH.SetUsers) + r.POST("/devices/:guid/remove_from_user", authMW, devH.RemoveFromUser) + + r.POST("/records/upload", authMW, recH.Upload) + r.GET("/records", authMW, recH.List) + r.GET("/records/:id/file", authMW, recH.File) + + // WebSocket livestream + r.GET("/livestream", authMW, liveH.Upgrade) + + // health + r.GET("/healthz", func(c *gin.Context) { c.String(http.StatusOK, "ok") }) + + // sensible defaults + r.MaxMultipartMemory = 64 << 20 // 64 MiB + _ = time.Now() // appease linters + return r +} + +// --- JWT middleware & helpers (kept here to avoid new dirs) --- diff --git a/server/internal/storage/minio.go b/server/internal/storage/minio.go new file mode 100644 index 0000000..34e9ca1 --- /dev/null +++ b/server/internal/storage/minio.go @@ -0,0 +1,60 @@ +package storage + +import ( + "context" + "fmt" + "net/url" + "strings" + "time" + + "github.com/minio/minio-go/v7" + "github.com/minio/minio-go/v7/pkg/credentials" +) + +type MinIOConfig struct { + Endpoint string + AccessKey string + SecretKey string + UseSSL bool + RecordsBucket string + LivestreamBucket string + PresignTTL time.Duration +} + +func cleanEndpoint(ep string) string { + ep = strings.TrimSpace(ep) + if ep == "" { + return ep + } + // If a scheme is present, strip it. + if u, err := url.Parse(ep); err == nil && u.Host != "" { + ep = u.Host // drops scheme and any path/query + } + // Remove any trailing slash that might remain. + ep = strings.TrimSuffix(ep, "/") + return ep +} + +func NewMinio(c MinIOConfig) (*minio.Client, error) { + endpoint := cleanEndpoint(c.Endpoint) + return minio.New(endpoint, &minio.Options{ + Creds: credentials.NewStaticV4(c.AccessKey, c.SecretKey, ""), + Secure: c.UseSSL, + }) +} + +func EnsureBuckets(mc *minio.Client, c MinIOConfig) error { + ctx := context.Background() + for _, b := range []string{c.RecordsBucket, c.LivestreamBucket} { + exists, err := mc.BucketExists(ctx, b) + if err != nil { + return err + } + if !exists { + if err := mc.MakeBucket(ctx, b, minio.MakeBucketOptions{}); err != nil { + return fmt.Errorf("make bucket %s: %w", b, err) + } + } + } + return nil +} diff --git a/server/internal/vault/vault.go b/server/internal/vault/vault.go new file mode 100644 index 0000000..d3131ff --- /dev/null +++ b/server/internal/vault/vault.go @@ -0,0 +1,41 @@ +package vault + +import ( + "context" + "fmt" + "time" + + vault "github.com/hashicorp/vault-client-go" +) + +func ReadKVv2(addr, token, mountPath, key string) (map[string]any, error) { + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + client, err := vault.New( + vault.WithAddress(addr), + vault.WithRequestTimeout(30*time.Second), + ) + if err != nil { + return nil, fmt.Errorf("vault new: %w", err) + } + if err := client.SetToken(token); err != nil { + return nil, fmt.Errorf("set token: %w", err) + } + + resp, err := client.Secrets.KvV2Read(ctx, key, vault.WithMountPath(mountPath)) + if err != nil { + return nil, err + } + if resp == nil || resp.Data.Data == nil { + return nil, fmt.Errorf("vault: empty response for %s/%s", mountPath, key) + } + return resp.Data.Data, nil +} + +// tiny typed error +type ErrNotFound string + +func (e ErrNotFound) Error() string { + return "vault: secret not found at " + string(e) +} diff --git a/traefik/.env b/traefik/.env new file mode 100644 index 0000000..0db0d97 --- /dev/null +++ b/traefik/.env @@ -0,0 +1 @@ +TRAEFIK_DASHBOARD_CREDENTIALS=admin:$$2y$$05$$PWLMUBMexlqDK6yXZvKWFeFaMULMlVb4Fp7.XUvFpvVhuqxeVLeMq \ No newline at end of file diff --git a/traefik/config/config.yaml b/traefik/config/config.yaml new file mode 100644 index 0000000..2bb9d51 --- /dev/null +++ b/traefik/config/config.yaml @@ -0,0 +1,19 @@ +http: + middlewares: + default-security-headers: + headers: + customBrowserXSSValue: 0 # X-XSS-Protection=1; mode=block + contentTypeNosniff: true # X-Content-Type-Options=nosniff + forceSTSHeader: true # Add the Strict-Transport-Security header even when the connection is HTTP + frameDeny: false # X-Frame-Options=deny + referrerPolicy: "strict-origin-when-cross-origin" + stsIncludeSubdomains: true # Add includeSubdomains to the Strict-Transport-Security header + stsPreload: true # Add preload flag appended to the Strict-Transport-Security header + stsSeconds: 3153600 # Set the max-age of the Strict-Transport-Security header (63072000 = 2 years) + contentSecurityPolicy: "default-src 'self'" + customRequestHeaders: + X-Forwarded-Proto: https + https-redirectscheme: + redirectScheme: + scheme: https + permanent: true \ No newline at end of file diff --git a/traefik/config/traefik.yaml b/traefik/config/traefik.yaml new file mode 100644 index 0000000..7b30fbc --- /dev/null +++ b/traefik/config/traefik.yaml @@ -0,0 +1,37 @@ +api: + dashboard: true + debug: true +entryPoints: + http: + address: ":80" + http: + # middlewares: # uncomment if using CrowdSec - see my video + # - crowdsec-bouncer@file + redirections: + entryPoint: + to: https + scheme: https + https: + address: ":443" + # http: + # middlewares: # uncomment if using CrowdSec - see my video + # - crowdsec-bouncer@file + # tcp: + # address: ":10000" + # apis: + # address: ":33073" +serversTransport: + insecureSkipVerify: true +providers: + docker: + endpoint: "unix:///var/run/docker.sock" + exposedByDefault: false + file: + filename: /config.yaml # example provided gives A+ rating https://www.ssllabs.com/ssltest/ + + +log: + level: "INFO" + filePath: "/var/log/traefik/traefik.log" +accessLog: + filePath: "/var/log/traefik/access.log" \ No newline at end of file diff --git a/traefik/docker-compose.yaml b/traefik/docker-compose.yaml new file mode 100644 index 0000000..2c70916 --- /dev/null +++ b/traefik/docker-compose.yaml @@ -0,0 +1,49 @@ +services: + traefik: + image: traefik:latest # or pin a specific v3.x version + container_name: traefik + restart: unless-stopped + security_opt: + - no-new-privileges:true + env_file: + - .env # should define TRAEFIK_DASHBOARD_CREDENTIALS + networks: + - proxy + ports: + - "80:80" + - "443:443" + environment: + - TRAEFIK_DASHBOARD_CREDENTIALS=${TRAEFIK_DASHBOARD_CREDENTIALS} + volumes: + - /etc/localtime:/etc/localtime:ro + - ${XDG_RUNTIME_DIR}/podman/podman.sock:/var/run/docker.sock:Z + # Static (static) config + - ./config/traefik.yaml:/traefik.yaml:ro,Z + # Dynamic (file provider) config + - ./config/config.yaml:/config.yaml:ro,Z + # Your self-signed wildcard cert + - ./certs/wildcard.192.168.205.130.nip.io.crt:/certs/wildcard.192.168.205.130.nip.io.crt:ro + - ./certs/wildcard.192.168.205.130.nip.io.key:/certs/wildcard.192.168.205.130.nip.io.key:ro + # Logs + - ./logs:/var/log/traefik:Z + labels: + - "traefik.enable=true" + - "traefik.http.routers.traefik.entrypoints=http" # expose dashboard on HTTP :contentReference[oaicite:2]{index=2} + - "traefik.http.routers.traefik.rule=Host(`traefik.192.168.205.130.nip.io`)" + - "traefik.http.middlewares.traefik-auth.basicauth.users=${TRAEFIK_DASHBOARD_CREDENTIALS}" + # Redirect HTTP → HTTPS + - "traefik.http.middlewares.traefik-https-redirect.redirectscheme.scheme=https" + - "traefik.http.routers.traefik.middlewares=traefik-https-redirect" + # Secure (HTTPS) dashboard + - "traefik.http.routers.traefik-secure.entrypoints=https" + - "traefik.http.routers.traefik-secure.rule=Host(`traefik.192.168.205.130.nip.io`)" + - "traefik.http.routers.traefik-secure.middlewares=traefik-auth" + - "traefik.http.routers.traefik-secure.tls=true" + - "traefik.http.routers.traefik-secure.tls.domains[0].main=192.168.205.130.nip.io" + - "traefik.http.routers.traefik-secure.tls.domains[0].sans=*.192.168.205.130.nip.io" + - "traefik.http.routers.traefik-secure.service=api@internal" + +networks: + proxy: + external: true +