diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..331e938 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @Jaro-c diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..6f4361d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: false +contact_links: + - name: Security vulnerability + url: https://github.com/Jaro-c/Lynx/security/advisories/new + about: Report a security vulnerability privately (do not open a public issue) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index e7577e5..ba9aed4 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -12,13 +12,13 @@ updates: target-branch: "develop" - package-ecosystem: "cargo" - directory: "/lynx/translators/compose" + directory: "/lynx" schedule: - interval: "weekly" + interval: "daily" labels: - "dependencies" - - "translator/compose" - open-pull-requests-limit: 5 + - "rust" + open-pull-requests-limit: 10 target-branch: "develop" - package-ecosystem: "bun" diff --git a/.github/scripts/audit-urls.py b/.github/scripts/audit-urls.py new file mode 100644 index 0000000..37cacc5 --- /dev/null +++ b/.github/scripts/audit-urls.py @@ -0,0 +1,77 @@ +#!/usr/bin/env python3 +"""Audit download modules for HTTP URLs outside the allowed GitHub domains. + +Only scans files that perform binary downloads (update modules and scheduler). +Other files (agent command endpoints, nginx configs) are intentionally excluded. + +Exits 1 if any non-allowed URL literal is found in non-comment lines. + +To suppress a specific line that is intentionally non-GitHub (e.g. a self health check), +add an inline comment: + .get("http://127.0.0.1:8080/health") // audit-urls: ok — reason + +Run from the repository root. +""" +import re +import sys +import pathlib + +# Only GitHub release domains are allowed for binary downloads. +ALLOWED = re.compile( + r"https?://" + r"(github\.com|objects\.githubusercontent\.com|api\.github\.com)" +) + +URL_RE = re.compile(r'https?://[^\s\'">,)]+') + +# Format strings / templates — skip lines containing these (not real URLs) +FORMAT_MARKERS = re.compile(r"\{[^}]*\}|\$\w+|%[sdfi]") + +COMMENT_RE = re.compile(r"^\s*//") +SUPPRESS_RE = re.compile(r"//\s*audit-urls:\s*ok") + +# Only files that perform outbound binary downloads. +# Adding a new download path outside these files requires a conscious update here. +SCAN_FILES = [ + "lynx/agent/src/update/mod.rs", + "lynx/agent/src/update/fallback.rs", + "lynx/dashboard/server/src/update.rs", + "lynx/dashboard/server/src/scheduler.rs", +] + +failures: list[str] = [] + +for path_str in SCAN_FILES: + f = pathlib.Path(path_str) + if not f.exists(): + print(f"⚠️ Scan target not found: {f} (skipped)") + continue + for i, line in enumerate(f.read_text(encoding="utf-8").splitlines(), 1): + if COMMENT_RE.match(line): + continue + if SUPPRESS_RE.search(line): + continue + if FORMAT_MARKERS.search(line): + continue + for url in URL_RE.findall(line): + if not ALLOWED.match(url): + failures.append(f" {f}:{i} {url}") + +if failures: + print("❌ Non-allowed URL found in download modules:") + for entry in failures: + print(entry) + print() + print("Allowed domains: github.com, objects.githubusercontent.com, api.github.com") + print() + print("If this URL is intentional (e.g. a self health check, not a download):") + print(" Add an inline suppression comment on that line:") + print(' .get("http://...") // audit-urls: ok — reason') + print() + print("If this is a new external download domain:") + print(" Add it to ALLOWED in .github/scripts/audit-urls.py with justification.") + sys.exit(1) + +scanned = ", ".join(SCAN_FILES) +print(f"✅ All HTTP URLs in download modules are from allowed domains.") +print(f" Scanned: {len(SCAN_FILES)} files") diff --git a/.github/scripts/sign.py b/.github/scripts/sign.py new file mode 100644 index 0000000..297e7d0 --- /dev/null +++ b/.github/scripts/sign.py @@ -0,0 +1,40 @@ +#!/usr/bin/env python3 +"""Sign a file with an Ed25519 private key (raw 32-byte seed, base64-encoded). + +Usage: + sign.py [] + +If output-sig-file is omitted, writes to .sig. +The key must be the raw 32-byte Ed25519 seed in standard base64. +""" +import base64 +import sys + +from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey + + +def main() -> None: + if len(sys.argv) < 3: + print(__doc__, file=sys.stderr) + sys.exit(1) + + key_b64 = sys.argv[1] + input_file = sys.argv[2] + sig_file = sys.argv[3] if len(sys.argv) > 3 else input_file + ".sig" + + key_bytes = base64.b64decode(key_b64 + "==") + private_key = Ed25519PrivateKey.from_private_bytes(key_bytes) + + with open(input_file, "rb") as f: + data = f.read() + + sig = private_key.sign(data) + + with open(sig_file, "wb") as f: + f.write(sig) + + print(f"signed {input_file} ({len(data):,} bytes) → {sig_file} ({len(sig)} bytes)") + + +if __name__ == "__main__": + main() diff --git a/.github/workflows/agent.yml b/.github/workflows/agent.yml index 5ac4d0e..045c99f 100644 --- a/.github/workflows/agent.yml +++ b/.github/workflows/agent.yml @@ -35,7 +35,7 @@ jobs: run: working-directory: lynx steps: - - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9 # stable with: @@ -45,6 +45,15 @@ jobs: - name: fmt run: cargo fmt --package lynx-agent -- --check + audit-urls: + name: Audit download URLs + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Check download URLs are from allowed domains + run: python3 .github/scripts/audit-urls.py + audit: name: Security audit runs-on: ubuntu-latest @@ -52,7 +61,7 @@ jobs: run: working-directory: lynx steps: - - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9 # stable with: @@ -85,7 +94,7 @@ jobs: --health-timeout 5s --health-retries 10 steps: - - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9 # stable with: @@ -129,7 +138,7 @@ jobs: --health-timeout 5s --health-retries 10 steps: - - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9 # stable with: diff --git a/.github/workflows/compose.yml b/.github/workflows/compose.yml index d6dfda7..f3dde5b 100644 --- a/.github/workflows/compose.yml +++ b/.github/workflows/compose.yml @@ -23,6 +23,25 @@ env: RUST_BACKTRACE: 1 jobs: + audit: + name: Security audit + runs-on: ubuntu-latest + defaults: + run: + working-directory: lynx + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9 # stable + with: + toolchain: stable + + - name: Install cargo-audit + run: cargo install cargo-audit --locked + + - name: audit + run: cargo audit --ignore RUSTSEC-2023-0071 + check: name: Check & Lint runs-on: ubuntu-latest @@ -30,7 +49,7 @@ jobs: run: working-directory: lynx steps: - - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9 # stable with: @@ -54,7 +73,7 @@ jobs: run: working-directory: lynx steps: - - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9 # stable with: diff --git a/.github/workflows/dashboard-server.yml b/.github/workflows/dashboard-server.yml index 586043a..e600bab 100644 --- a/.github/workflows/dashboard-server.yml +++ b/.github/workflows/dashboard-server.yml @@ -33,7 +33,7 @@ jobs: run: working-directory: lynx steps: - - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9 # stable with: @@ -43,6 +43,15 @@ jobs: - name: fmt run: cargo fmt --package lynx-dashboard-server -- --check + audit-urls: + name: Audit download URLs + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Check download URLs are from allowed domains + run: python3 .github/scripts/audit-urls.py + audit: name: Security audit runs-on: ubuntu-latest @@ -50,7 +59,7 @@ jobs: run: working-directory: lynx steps: - - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9 # stable with: @@ -83,7 +92,7 @@ jobs: --health-timeout 5s --health-retries 10 steps: - - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9 # stable with: @@ -136,7 +145,7 @@ jobs: --health-timeout 5s --health-retries 10 steps: - - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9 # stable with: @@ -155,7 +164,7 @@ jobs: DATABASE_URL: postgres://lynx_ci:lynx_ci@localhost:5432/lynx_ci - name: test - run: cargo test --package lynx-dashboard-server + run: cargo test --package lynx-dashboard-server -- --test-threads=1 env: DATABASE_URL: postgres://lynx_ci:lynx_ci@localhost:5432/lynx_ci REDIS_URL: redis://localhost:6379 diff --git a/.github/workflows/dashboard-ui.yml b/.github/workflows/dashboard-ui.yml index c9044ed..ac32103 100644 --- a/.github/workflows/dashboard-ui.yml +++ b/.github/workflows/dashboard-ui.yml @@ -17,6 +17,25 @@ permissions: security-events: write jobs: + audit: + name: Security audit (bun) + runs-on: ubuntu-latest + defaults: + run: + working-directory: lynx/dashboard/ui + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 + with: + bun-version: latest + + - name: Install dependencies + run: bun install --frozen-lockfile + + - name: audit + run: bun audit --audit-level high + typecheck: name: TypeScript runs-on: ubuntu-latest @@ -24,7 +43,7 @@ jobs: run: working-directory: lynx/dashboard/ui steps: - - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 with: @@ -43,7 +62,7 @@ jobs: run: working-directory: lynx/dashboard/ui steps: - - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 with: @@ -62,7 +81,7 @@ jobs: run: working-directory: lynx/dashboard/ui steps: - - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 with: @@ -86,7 +105,7 @@ jobs: BACKEND_URL: http://localhost:8080 - name: Upload Playwright report - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 if: failure() with: name: playwright-report diff --git a/.github/workflows/lint-shell.yml b/.github/workflows/lint-shell.yml index 947141c..db41f87 100644 --- a/.github/workflows/lint-shell.yml +++ b/.github/workflows/lint-shell.yml @@ -9,8 +9,6 @@ on: pull_request: branches: - main - paths: - - "**.sh" permissions: contents: read @@ -21,7 +19,7 @@ jobs: name: shellcheck runs-on: ubuntu-latest steps: - - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Run shellcheck run: bash scripts/lint.sh diff --git a/.github/workflows/release-agent.yml b/.github/workflows/release-agent.yml new file mode 100644 index 0000000..7f4f170 --- /dev/null +++ b/.github/workflows/release-agent.yml @@ -0,0 +1,90 @@ +name: release-agent + +on: + push: + tags: + - "agent@*" + +permissions: + contents: write + +jobs: + build: + name: Build ${{ matrix.arch }} + runs-on: ${{ matrix.runs-on }} + + strategy: + fail-fast: false + matrix: + include: + - runs-on: ubuntu-latest + arch: x86_64 + rust-target: x86_64-unknown-linux-musl + + - runs-on: ubuntu-24.04-arm + arch: arm64 + rust-target: aarch64-unknown-linux-musl + + defaults: + run: + working-directory: lynx + + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9 # stable + with: + toolchain: stable + targets: ${{ matrix.rust-target }} + + - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 + with: + workspaces: lynx + + - name: Install musl toolchain + run: sudo apt-get install -y musl-tools + + - name: Build agent (musl static) + run: | + cargo build --release \ + --package lynx-agent \ + --target ${{ matrix.rust-target }} + + - name: Sign agent binary + working-directory: ${{ github.workspace }} + env: + RELEASE_SIGN_KEY: ${{ secrets.RELEASE_SIGN_KEY }} + run: | + pip install --quiet cryptography + BINARY="lynx/target/${{ matrix.rust-target }}/release/lynx-agent" + ARTIFACT="lynx-agent-linux-${{ matrix.arch }}" + cp "$BINARY" "$ARTIFACT" + python3 .github/scripts/sign.py "$RELEASE_SIGN_KEY" "$ARTIFACT" + + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + with: + name: agent-${{ matrix.arch }} + path: | + lynx-agent-linux-${{ matrix.arch }} + lynx-agent-linux-${{ matrix.arch }}.sig + retention-days: 1 + + release: + name: Publish release + needs: build + runs-on: ubuntu-latest + + steps: + - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 + with: + merge-multiple: true + + - name: Create GitHub Release + uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2 + with: + generate_release_notes: true + files: | + lynx-agent-linux-x86_64 + lynx-agent-linux-x86_64.sig + lynx-agent-linux-arm64 + lynx-agent-linux-arm64.sig diff --git a/.github/workflows/release-dashboard.yml b/.github/workflows/release-dashboard.yml new file mode 100644 index 0000000..70b6cf5 --- /dev/null +++ b/.github/workflows/release-dashboard.yml @@ -0,0 +1,213 @@ +name: release-dashboard + +on: + push: + tags: + - "dashboard@*" + +permissions: + contents: write + +jobs: + prepare: + name: Verify sqlx cache + runs-on: ubuntu-latest + + services: + postgres: + image: postgres:18 + env: + POSTGRES_DB: lynx_dashboard + POSTGRES_USER: postgres + POSTGRES_PASSWORD: ci_test + ports: + - 5432:5432 + options: >- + --health-cmd pg_isready + --health-interval 5s + --health-timeout 3s + --health-retries 10 + + defaults: + run: + working-directory: lynx + + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9 # stable + with: + toolchain: stable + + - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 + with: + workspaces: lynx + + - name: Install sqlx-cli + run: cargo install sqlx-cli --no-default-features --features postgres --locked + + - name: Run migrations + env: + DATABASE_URL: postgresql://postgres:ci_test@localhost:5432/lynx_dashboard + run: sqlx migrate run --source dashboard/server/migrations + + - name: Verify sqlx cache is up-to-date + env: + DATABASE_URL: postgresql://postgres:ci_test@localhost:5432/lynx_dashboard + run: cargo sqlx prepare --check --package lynx-dashboard-server + + build: + name: Build ${{ matrix.arch }} + needs: prepare + runs-on: ${{ matrix.runs-on }} + + strategy: + fail-fast: false + matrix: + include: + - runs-on: ubuntu-latest + arch: x86_64 + rust-target: x86_64-unknown-linux-musl + bun-arch: x64 + + - runs-on: ubuntu-24.04-arm + arch: arm64 + rust-target: aarch64-unknown-linux-musl + bun-arch: aarch64 + + defaults: + run: + working-directory: lynx + + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + # --- Rust backend ------------------------------------------------------- + + - uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9 # stable + with: + toolchain: stable + targets: ${{ matrix.rust-target }} + + - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 + with: + workspaces: lynx + + - name: Install musl toolchain + run: sudo apt-get install -y musl-tools + + - name: Build backend (musl static) + run: | + cargo build --release \ + --package lynx-dashboard-server \ + --target ${{ matrix.rust-target }} + env: + SQLX_OFFLINE: "true" + + - name: Sign backend binary + working-directory: ${{ github.workspace }} + env: + RELEASE_SIGN_KEY: ${{ secrets.RELEASE_SIGN_KEY }} + run: | + pip install --quiet cryptography + BINARY="lynx/target/${{ matrix.rust-target }}/release/lynx-dashboard-server" + ARTIFACT="lynx-dashboard-backend-linux-${{ matrix.arch }}" + cp "$BINARY" "$ARTIFACT" + python3 .github/scripts/sign.py "$RELEASE_SIGN_KEY" "$ARTIFACT" + + # --- Frontend ----------------------------------------------------------- + + - name: Install musl bun + working-directory: ${{ github.workspace }} + run: | + BUN_ARCH="${{ matrix.bun-arch }}" + VERSION=$(curl -s "https://api.github.com/repos/oven-sh/bun/releases/latest" \ + | grep '"tag_name"' | head -1 | cut -d'"' -f4) + BASE_URL="https://github.com/oven-sh/bun/releases/download/${VERSION}" + curl -sL "${BASE_URL}/bun-linux-${BUN_ARCH}-musl.zip" -o bun.zip + curl -sL "${BASE_URL}/SHASUMS256.txt" -o SHASUMS256.txt + grep "bun-linux-${BUN_ARCH}-musl.zip" SHASUMS256.txt | sha256sum -c - + unzip -q bun.zip + sudo mv "bun-linux-${BUN_ARCH}-musl/bun" /usr/local/bin/bun + bun --version + + - name: Install frontend deps + working-directory: lynx/dashboard/ui + run: bun install --frozen-lockfile + + - name: Build Next.js (standalone) + working-directory: lynx/dashboard/ui + run: bun run build + env: + NEXT_TELEMETRY_DISABLED: "1" + + - name: Compile standalone binary (musl) + working-directory: lynx/dashboard/ui + run: | + bun build --compile \ + .next/standalone/server.js \ + --outfile "lynx-dashboard-frontend-linux-${{ matrix.arch }}" + + - name: Bundle static assets + working-directory: lynx/dashboard/ui + run: | + TARBALL="lynx-dashboard-frontend-assets-linux-${{ matrix.arch }}.tar.gz" + tar -czf "$TARBALL" \ + -C .next/standalone \ + .next/static \ + public + echo "assets tarball: $(du -sh "$TARBALL" | cut -f1)" + + - name: Sign frontend artifacts + working-directory: ${{ github.workspace }} + env: + RELEASE_SIGN_KEY: ${{ secrets.RELEASE_SIGN_KEY }} + run: | + BIN="lynx/dashboard/ui/lynx-dashboard-frontend-linux-${{ matrix.arch }}" + ASSETS="lynx/dashboard/ui/lynx-dashboard-frontend-assets-linux-${{ matrix.arch }}.tar.gz" + python3 .github/scripts/sign.py "$RELEASE_SIGN_KEY" "$BIN" + python3 .github/scripts/sign.py "$RELEASE_SIGN_KEY" "$ASSETS" + + # --- Upload artifacts --------------------------------------------------- + + - name: Upload release artifacts + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + with: + name: dashboard-${{ matrix.arch }} + path: | + lynx/target/${{ matrix.rust-target }}/release/lynx-dashboard-server + lynx-dashboard-backend-linux-${{ matrix.arch }} + lynx-dashboard-backend-linux-${{ matrix.arch }}.sig + lynx/dashboard/ui/lynx-dashboard-frontend-linux-${{ matrix.arch }} + lynx/dashboard/ui/lynx-dashboard-frontend-linux-${{ matrix.arch }}.sig + lynx/dashboard/ui/lynx-dashboard-frontend-assets-linux-${{ matrix.arch }}.tar.gz + lynx/dashboard/ui/lynx-dashboard-frontend-assets-linux-${{ matrix.arch }}.tar.gz.sig + retention-days: 1 + + release: + name: Publish release + needs: build + runs-on: ubuntu-latest + + steps: + - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 + with: + merge-multiple: true + + - name: Create GitHub Release + uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2 + with: + generate_release_notes: true + files: | + lynx-dashboard-backend-linux-x86_64 + lynx-dashboard-backend-linux-x86_64.sig + lynx-dashboard-backend-linux-arm64 + lynx-dashboard-backend-linux-arm64.sig + lynx-dashboard-frontend-linux-x86_64 + lynx-dashboard-frontend-linux-x86_64.sig + lynx-dashboard-frontend-assets-linux-x86_64.tar.gz + lynx-dashboard-frontend-assets-linux-x86_64.tar.gz.sig + lynx-dashboard-frontend-linux-arm64 + lynx-dashboard-frontend-linux-arm64.sig + lynx-dashboard-frontend-assets-linux-arm64.tar.gz + lynx-dashboard-frontend-assets-linux-arm64.tar.gz.sig diff --git a/.gitignore b/.gitignore index 800828e..8f7237f 100644 --- a/.gitignore +++ b/.gitignore @@ -31,6 +31,15 @@ CLAUDE.md .env.local .env.*.local +# Playwright +**/playwright-report/ +**/test-results/ +.playwright-mcp/ +**/.playwright-mcp/ + +# FUSE filesystem temp files (Linux file manager / gvfs) +.fuse_hidden* + # Editor *.swp *.swo diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..0a79947 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,35 @@ +# Code of Conduct + +## Our Standards + +Lynx is a technical project. Interactions here — issues, PRs, discussions, comments — should be focused, respectful, and constructive. + +**Expected behavior:** +- Be direct and technically precise +- Critique code and ideas, not people +- Accept feedback gracefully — the goal is a better project +- Help others when you can + +**Unacceptable behavior:** +- Harassment, personal attacks, or discriminatory language +- Deliberate intimidation or trolling +- Publishing others' private information without consent +- Any conduct that would be considered unprofessional in a technical setting + +--- + +## Enforcement + +Violations can be reported to the maintainer privately: +[**Report via GitHub →**](https://github.com/Jaro-c/Lynx/security/advisories/new) +Reports will be reviewed promptly and handled with discretion. The maintainer reserves the right to remove comments, close issues, or ban contributors who violate these standards. + +--- + +## Scope + +This Code of Conduct applies to all project spaces — GitHub issues, PRs, discussions, and any other official project channels. + +--- + +*Based on the [Contributor Covenant](https://www.contributor-covenant.org/), adapted for Lynx.* diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..9529567 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,176 @@ +# Contributing to Lynx + +Lynx is a self-hosted VPS & container manager built in Rust and Next.js. Contributions are welcome — bug fixes, features, tests, and documentation. + +Contributions are voluntary and unpaid. If you find Lynx useful and want to support its development, [GitHub Sponsors](https://github.com/sponsors/Jaro-c) is appreciated. + +--- + +## Before You Start + +- Search existing issues and PRs before opening new ones +- For large changes, open an issue first to discuss the approach +- All contributions must be in English — code, comments, commits, PR descriptions + +--- + +## Branches + +| Branch | Purpose | +|--------|---------| +| `main` | Production. Never push directly. Merge via PR from `develop` only. | +| `develop` | Working branch. Direct push allowed for maintainers. PRs target this branch. | + +--- + +## Development Setup + +**Agent (Rust):** +```bash +cd lynx +cargo build -p lynx-agent +cargo test -p lynx-agent +``` + +**Dashboard backend (Rust):** +```bash +cd lynx +cargo build -p lynx-dashboard-server +cargo test -p lynx-dashboard-server +``` + +**Dashboard frontend (Next.js):** +```bash +cd lynx/dashboard/ui +bun install +bun dev +``` + +**Lint:** +```bash +bash scripts/lint.sh # shellcheck on all .sh files +``` + +--- + +## Pull Requests + +- Squash merge only — one commit per PR on `main` +- Commits must be **GPG or SSH signed** — unsigned commits are rejected +- Keep PRs focused: one concern per PR +- Fill out the PR template completely + +--- + +## Commit Messages + +Conventional Commits format: + +``` +feat(agent): add PSK rotation without tunnel restart +fix(dashboard): correct nonce cleanup interval +chore(ci): update ubuntu runner to 24.04 +``` + +Subject ≤ 50 characters. Body only when the "why" isn't obvious from the code. + +--- + +## Versioning + +Two independent release tracks: + +- `dashboard@x.y.z` — dashboard backend + frontend +- `agent@x.y.z` — agent binary + +A release on one track does not require a release on the other. Tags trigger the corresponding release workflow. + +--- + +## Code Style + +**Rust (agent + dashboard backend):** +- `cargo fmt` before committing +- `cargo clippy -- -D warnings` must pass +- No `unwrap()` in production paths — use proper error handling +- UTC everywhere — no local timestamps +- UUID v7 for all table IDs +- Queries via `sqlx` with bound parameters only — no string interpolation in SQL +- Shell commands via `std::process::Command::arg()` — never `sh -c "...{input}..."` + +**Next.js (dashboard frontend):** +- `biome format` + `biome lint` before committing +- Server Components by default — `"use client"` only when required +- All user-visible text in i18n files (`en.json`, `es.json`) — never hardcoded strings +- Zod schemas for all input validation + +**Shell scripts:** +- `shellcheck` must pass (`bash scripts/lint.sh`) +- Header block required (description, usage, requirements) +- ANSI colors for output + +--- + +## Tests + +**CI (GitHub Actions) — runs automatically on every PR:** +- Rust unit tests (`cargo test`) +- Dashboard integration tests (PostgreSQL + Redis containers) +- Frontend tests (Vitest + Playwright) +- `cargo-audit` — fails on known CVEs +- `bun audit` — fails on high/critical npm vulnerabilities +- `shellcheck` on all `.sh` files + +**VM tests — required for certain changes:** + +Some features cannot be tested in CI (nftables, WireGuard, Podman, systemd). These require local VMs: + +| Area | Environment | +|------|-------------| +| nftables rules, divergence detection | VM local | +| WireGuard tunnel setup, PSK rotation | VM local (CAP_NET_ADMIN) | +| Podman containers, org isolation | VM local | +| Auto-update binary swap | VM local | +| Installation + incompatible software | VM local | +| Agent ↔ dashboard connectivity | 2 VMs | +| Migration (dashboard or agent) | 2–3 VMs | + +If your change affects these areas, note in your PR which VM scenarios you ran. + +--- + +## What Not to Contribute + +- Docker support — incompatible by design (nftables/network isolation conflict) +- Rollback / downgrade mechanisms — hotfix + auto-update is the model +- Metrics persistence — metrics are real-time WebSocket only +- SMTP integration — not planned +- Changes that break backwards compatibility of migrations (additive-only) + +
+File organization reference +
+ +Never accumulate many files in one folder. Always use subdirectories by responsibility. + +Rust pattern: +``` +agents/ +├── mod.rs +├── router.rs +├── heartbeat.rs +└── handlers/ + ├── mod.rs ← re-exports only + ├── crud.rs + └── commands.rs +``` + +Frontend mirrors the URL structure: +``` +src/components/(dashboard)/agents/ +├── list/ +├── detail/ +└── nftables/ +``` + +
diff --git a/README.md b/README.md new file mode 100644 index 0000000..ca8398f --- /dev/null +++ b/README.md @@ -0,0 +1,185 @@ +
+ Lynx

+ + # Lynx + + **Self-hosted VPS & container manager.**
+ Containers · Firewall · VPN — from one dashboard, across any number of servers. + +
+ + [![CI — Agent](https://github.com/Jaro-c/Lynx/actions/workflows/agent.yml/badge.svg)](https://github.com/Jaro-c/Lynx/actions/workflows/agent.yml) + [![CI — Dashboard](https://github.com/Jaro-c/Lynx/actions/workflows/dashboard-server.yml/badge.svg)](https://github.com/Jaro-c/Lynx/actions/workflows/dashboard-server.yml) + [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE) + ![Rust](https://img.shields.io/badge/Agent-Rust-orange?logo=rust) + ![Next.js](https://img.shields.io/badge/Dashboard-Next.js-black?logo=next.js) + +
+ + [Install](#-install) · [Architecture](#-architecture) · [vs Alternatives](#-vs-alternatives) · [Security](#-security) + +
+ +--- + +> **The cPanel/Plesk/Coolify alternative built for people who care about security.** +> One binary per VPS. All traffic encrypted over WireGuard. No SaaS. No cloud lock-in. No Docker daemon. + +--- + +## ✨ Features + +**📦 Containers** — Podman rootless, per-organization isolation, survive VPS reboots without Lynx running +**🔥 Firewall** — Full nftables control from the dashboard, three-layer hierarchy, atomic apply, auto-restore on any tampering +**🔒 Networking** — All dashboard → agent traffic over WireGuard + mTLS. Cross-VPS scaling via direct agent tunnels — no relay through dashboard +**🔑 Encryption** — PostgreSQL AES-256 at rest (pg_tde) + per-user envelope encryption (KEK/DEK) +**📁 Single binary** — No runtime dependencies on the server. No Node.js, no Bun, no Docker Engine. Install one binary, uninstall one binary +**🔄 Auto-update** — Hourly GitHub Releases check, Ed25519 signature verification before any swap, automatic rollback if the new binary fails to start + +--- + +## 🏗 Architecture + +``` +Dashboard VPS +├── Frontend ── Next.js (compiled binary, no runtime) +├── Backend ── Rust +│ ├── WireGuard ──► Agent (local, same VPS) +│ ├── WireGuard ──► Agent (remote VPS #1) +│ └── WireGuard ──► Agent (remote VPS #2) +│ +└── Each agent: Podman + nftables + WireGuard +``` + +Each agent connects to the dashboard over a **1:1 WireGuard tunnel** with its own PSK. Agents never talk to each other through the dashboard — cross-VPS scaling uses direct agent-to-agent tunnels. + +
+Firewall hierarchy (nftables) +
+ +``` +table inet lynx-agent { + chain lynx-base ← Lynx invariants. Never editable. Auto-restored instantly on any change. + chain lynx-global ← Rules pushed to ALL agents simultaneously + chain lynx-local ← Per-VPS rules for this agent only +} +``` + +- **`lynx-base`** — default deny, WireGuard allowlist, inter-org isolation, anti-spoofing +- **`lynx-global`** — IP blocklists, protocol restrictions — propagated to all agents in parallel; agents offline receive pending rules on reconnect +- **`lynx-local`** — per-VPS port rules, IP allowlists + +
+ +
+Horizontal scaling — cross-VPS +
+ +``` +Internet → 80/443 + ↓ +lynx-nginx (Agent-1, entry point) + ├── replica:1 (Agent-1, local Podman network) + └── WireGuard ──► Agent-2 + ├── replica:2 + └── replica:3 +``` + +Agent-2 never exposes public ports for the project. All traffic enters through Agent-1 via WireGuard. + +
+ +--- + +## ⚡ Install + +### Dashboard + +```bash +curl -fsSL https://raw.githubusercontent.com/Jaro-c/Lynx/main/install.sh | sudo bash +``` + +The installer handles everything: +1. Detects and removes incompatible software (Docker, firewalld, ufw, iptables) +2. Installs Podman, WireGuard, nftables +3. Generates all secrets — never written to disk in plaintext +4. Starts PostgreSQL → Redis → Backend → Frontend +5. Prints a one-time setup URL: + ``` + https://YOUR-IP:19443/register?setup_token= + ``` + +### Agent (additional VPS) + +1. Dashboard → **Connect new VPS** → copy the displayed keypair + PSK +2. On the new VPS, run the same installer and paste the dashboard data when prompted +3. Done — the tunnel is up and the agent appears online in the dashboard + +### Requirements + +| | | +|---|---| +| **OS** | Ubuntu 22.04+, Debian 12+, Fedora 39+, CentOS/RHEL 9+, Rocky/AlmaLinux 9+ | +| **SSH port** | Auto-detected — any port works | +| **Fixed ports** | `19443/TCP` (dashboard) · `51820/UDP` (WireGuard) — opened automatically. Must be free and allowed by your VPS provider's external firewall if applicable. | +| **Root access** | Required for install | + +--- + +## 🆚 vs Alternatives + +| | **Lynx** | Coolify | Dokploy | cPanel / Plesk | +|---|---|---|---|---| +| Container runtime | Podman (rootless) | Docker | Docker | varies | +| Firewall management | ✅ Full nftables | ❌ | ❌ | Partial | +| VPN between servers | ✅ WireGuard | ❌ | ❌ | ❌ | +| Encryption at rest | ✅ AES-256 (pg_tde) | ❌ | ❌ | ❌ | +| Per-user encryption | ✅ KEK/DEK | ❌ | ❌ | ❌ | +| Signed binary updates | ✅ Ed25519 | ❌ | ❌ | ❌ | +| Runtime dependencies | None | Docker Engine | Docker Engine | Heavy | +| Pricing | Free / self-hosted | Free tier + paid | Free / self-hosted | Paid license | +| SaaS / cloud | Never | Optional | Optional | Optional | + +--- + +## 🔐 Security + +
+Transport & cryptography +
+ +- **WireGuard + mTLS** — double-layer encryption on all dashboard ↔ agent traffic +- **TLS 1.3 minimum** — no TLS 1.0/1.1/1.2 accepted anywhere +- **Ed25519** — JWT signing, agent command signing, and binary update verification +- **Per-agent PSK** — each tunnel has its own unique preshared key, rotated automatically + +
+ +
+Signed commands & immutable audit log +
+ +Every command the dashboard sends to an agent is Ed25519-signed. The agent verifies signature, nonce (replay prevention), and timestamp (< 30s window) before executing anything. + +All executed and rejected commands are stored in a **hash-chained append-only audit log** on the agent, synced to dashboard PostgreSQL in real time. Tampering with any entry is mathematically detectable. + +
+ +
+Reporting a vulnerability +
+ +See [SECURITY.md](SECURITY.md). + +
+ +--- + +## 📄 License + +[MIT](LICENSE) — © 2026 [Jaro-c](https://github.com/Jaro-c) + +
+
+ Made with ❤️ by Jaroc +
diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..91919bc --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,81 @@ +# Security Policy + +--- + +## Reporting a Vulnerability + +**Do not open a public GitHub issue for security vulnerabilities.** + +Report via GitHub's private vulnerability disclosure: +[**Report a vulnerability →**](https://github.com/Jaro-c/Lynx/security/advisories/new) + +Include: +- Component affected (`dashboard` / `agent` / installer) +- Description of the vulnerability +- Steps to reproduce +- Potential impact +- Suggested fix (optional) + +Responsible disclosure is appreciated. If the report leads to a fix, you'll be credited in the release notes (unless you prefer anonymity). + +--- + +## Response Timeline + +| Stage | Target | +|-------|--------| +| Acknowledgement | 48 hours | +| Initial assessment | 5 business days | +| Fix + release | Depends on severity | + +**Critical** (RCE, auth bypass, crypto break, privilege escalation) — target fix within 7 days. +**High** (data leak, firewall bypass, replay attack) — target fix within 14 days. +**Medium / Low** — addressed in next regular release. + +--- + +## Supported Versions + +Only the latest release of each component is supported. Lynx auto-updates itself — there is no manual rollback. If a critical bug is found, a new release is published and deployed automatically. + +| Component | Support | +|-----------|---------| +| `dashboard@latest` | ✅ Supported | +| `agent@latest` | ✅ Supported | +| Older versions | ❌ No patches — update via auto-update | + +--- + +## Scope + +**In scope:** +- Authentication and session handling +- WireGuard tunnel security +- nftables rule bypass +- Command signature verification (Ed25519) +- Replay / nonce attacks +- Privilege escalation via agent or containers +- Envelope encryption (KEK/DEK) +- PostgreSQL TDE key handling +- Auto-update pipeline (Ed25519 binary signature) +- SSRF in binary download flow +- SQL injection, shell injection + +**Out of scope:** +- Vulnerabilities requiring physical access to the VPS +- Issues in third-party dependencies (report upstream — we track them via `cargo-audit` / `bun audit`) +- Theoretical attacks with no practical exploit path +- Social engineering + +--- + +## Security Architecture + +Key properties for threat modeling: + +- **Transport** — WireGuard + mTLS on all dashboard ↔ agent traffic. Agent never accepts plain connections. +- **Command integrity** — every dashboard → agent command is Ed25519-signed with a nonce and 30s timestamp window. Replay attacks rejected even if transport is compromised. +- **Binary integrity** — Ed25519 signature verified before any binary swap during auto-update. Partial downloads fail verification automatically. +- **Audit log** — hash-chained, append-only, synced to dashboard PostgreSQL in real time. Any tampered entry breaks the chain. +- **Firewall** — nftables default deny. `lynx-base` chain is invariant — auto-restored silently if modified, even by root. +- **Containers** — rootless Podman under per-org system users. UID 0 inside a container maps to an unprivileged UID on the host. diff --git a/docs/.nojekyll b/docs/.nojekyll new file mode 100644 index 0000000..e69de29 diff --git a/docs/index.html b/docs/index.html new file mode 100644 index 0000000..c905559 --- /dev/null +++ b/docs/index.html @@ -0,0 +1,313 @@ + + + + + + Lynx — Infrastructure Orchestration + + + + + + + + + + + + + +
+
+
+
+ +
+
+ + Self-hosted · Open source · Rust-powered +
+ +

+ Infrastructure
+ you control. +

+ +

+ Manage distributed VPS nodes from a single, self-hosted dashboard. + Every command signed. Every tunnel encrypted. No cloud, no vendor lock-in. +

+ + +
+
+ + +
+
+

// how it fits together

+
+
┌──────────────────────────────────────────────────────┐
+│  DASHBOARD VPS                                        │
+│  ┌────────────┐   ┌────────────┐   ┌─────────────┐  │
+│  │  Frontend  │──▶│  Backend   │──▶│ Local Agent │  │
+│  │  Next.js   │   │    Rust    │   │    Rust     │  │
+│  └────────────┘   └────────────┘   └─────────────┘  │
+│                         │                            │
+└─────────────────────────┼────────────────────────────┘
+                    WireGuard + mTLS
+           ┌──────────────┴──────────────┐
+           │                             │
+┌──────────┴───────┐        ┌────────────┴─────┐
+│  AGENT VPS        │        │  AGENT VPS       │
+│  ┌─────────────┐ │        │  ┌─────────────┐ │
+│  │  lynx-agent │ │        │  │  lynx-agent │ │
+│  │   Podman    │ │        │  │   Podman    │ │
+│  │   nftables  │ │        │  │   nftables  │ │
+│  └─────────────┘ │        │  └─────────────┘ │
+└──────────────────┘        └──────────────────┘
+
+
+
+ + +
+
+
+

Built for security. Designed for control.

+

Every design decision prioritizes isolation, auditability, and zero trust.

+
+ +
+ +
+
🔐
+

WireGuard-First

+

All dashboard ↔ agent traffic travels over WireGuard with unique PSKs per tunnel. No exceptions — even the local agent on the same VPS uses WireGuard.

+
+ +
+
📦
+

Single Binary

+

Agent and dashboard backend are standalone Rust binaries. No runtime dependencies. Install, run, done. Frontend compiled with Bun — no Node.js on the server.

+
+ +
+
🐋
+

Rootless Containers

+

Podman rootless per-organization isolation. Each org runs in its own user namespace with dedicated subuid/subgid ranges. Container UID 0 maps to unprivileged host UID.

+
+ +
+
🛡️
+

nftables Firewall

+

Automated firewall via nftables with three hierarchical chains. Divergence auto-detected and auto-restored from PostgreSQL source of truth. Tamper-evident by design.

+
+ +
+
✍️
+

Ed25519 Signatures

+

Every command signed with Ed25519. Agent verifies signature, nonce freshness, and timestamp (<30s window) before executing. Replay attacks impossible.

+
+ +
+
+

Auto-Update

+

Hourly scheduler checks GitHub Releases. Signed binary swap with automatic rollback to .prev if the new binary fails to start within 30s.

+
+ +
+
🔒
+

Encrypted Storage

+

PostgreSQL 18 with pg_tde AES-256 at the storage layer. Per-user envelope encryption (KEK/DEK). Argon2id for passwords. Secrets live only in tmpfs — never on disk.

+
+ +
+
📋
+

Immutable Audit Log

+

Hash-chained audit log on every agent — every command executed or rejected is logged. Tampering is mathematically detectable. Syncs in real-time to the dashboard.

+
+ +
+
🌐
+

Multi-VPS Orchestration

+

One dashboard. Unlimited agents. Scale horizontally across VPS nodes with cross-agent WireGuard data plane tunnels. Real-time metrics via WebSocket — no polling.

+
+ +
+
+
+ + +
+
+
+

Up and running in minutes.

+

One script installs everything. No Kubernetes. No Docker. No cloud accounts.

+
+ +
+
+
1
+
+

Install the dashboard

+

Run the install script on your VPS. PostgreSQL, Redis, WireGuard, and all components configured automatically with secure random secrets.

+
+ $ curl -sSL https://github.com/Jaro-c/Lynx/releases/latest/download/install-dashboard.sh | bash +
+
+
+ +
+
2
+
+

Connect remote VPS nodes

+

Generate WireGuard keys in the dashboard UI. Run the agent install script on each remote VPS — it asks for those keys and sets up the encrypted tunnel automatically.

+
+ $ curl -sSL https://github.com/Jaro-c/Lynx/releases/latest/download/install-agent.sh | bash +
+
+
+ +
+
3
+
+

Deploy and manage

+

Open https://your-vps:19443 — deploy containers, manage firewall rules, monitor real-time metrics, rotate keys, migrate nodes. All from one place, over encrypted tunnels.

+
+
+
+
+
+ + +
+
+

// built with

+
+ Rust + Next.js 16 + PostgreSQL 18 + pg_tde + WireGuard + Podman + nftables + Redis 8 + Ed25519 + mTLS + Argon2id +
+
+
+ + +
+
+
+

Own your infrastructure.

+

No cloud accounts. No vendor lock-in. No monthly subscriptions.
Just your servers, under your control.

+ + + + + Get Started on GitHub + +
+
+
+ + + + + + diff --git a/lynx/.sqlx/query-01bfbb58c095c306159c29c82daf5a72a9c1cf292d72b84f4a78bd1c18839b79.json b/lynx/.sqlx/query-01bfbb58c095c306159c29c82daf5a72a9c1cf292d72b84f4a78bd1c18839b79.json new file mode 100644 index 0000000..fd496a9 --- /dev/null +++ b/lynx/.sqlx/query-01bfbb58c095c306159c29c82daf5a72a9c1cf292d72b84f4a78bd1c18839b79.json @@ -0,0 +1,17 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO user_roles (id, user_id, role_id, created_by) VALUES ($1, $2, $3, $4)", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid", + "Uuid", + "Uuid", + "Uuid" + ] + }, + "nullable": [] + }, + "hash": "01bfbb58c095c306159c29c82daf5a72a9c1cf292d72b84f4a78bd1c18839b79" +} diff --git a/lynx/.sqlx/query-039956bbaa6983ec2bc328db9bc1c0c9b088c6e612d88bae53f9e1466b8b48f3.json b/lynx/.sqlx/query-039956bbaa6983ec2bc328db9bc1c0c9b088c6e612d88bae53f9e1466b8b48f3.json new file mode 100644 index 0000000..1c3a455 --- /dev/null +++ b/lynx/.sqlx/query-039956bbaa6983ec2bc328db9bc1c0c9b088c6e612d88bae53f9e1466b8b48f3.json @@ -0,0 +1,52 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT o.id, o.name, o.slug, o.owner_id, o.created_at,\n COUNT(m.user_id) AS \"member_count!\"\n FROM organizations o\n JOIN organization_members m ON m.organization_id = o.id\n WHERE m.user_id = $1\n GROUP BY o.id\n ORDER BY o.created_at ASC\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "name", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "slug", + "type_info": "Text" + }, + { + "ordinal": 3, + "name": "owner_id", + "type_info": "Uuid" + }, + { + "ordinal": 4, + "name": "created_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 5, + "name": "member_count!", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [ + false, + false, + false, + false, + false, + null + ] + }, + "hash": "039956bbaa6983ec2bc328db9bc1c0c9b088c6e612d88bae53f9e1466b8b48f3" +} diff --git a/lynx/.sqlx/query-04033eaad30ce63f08958fabfb5604aab131889512feeef9e61d33e07051ce42.json b/lynx/.sqlx/query-04033eaad30ce63f08958fabfb5604aab131889512feeef9e61d33e07051ce42.json new file mode 100644 index 0000000..09bf9c4 --- /dev/null +++ b/lynx/.sqlx/query-04033eaad30ce63f08958fabfb5604aab131889512feeef9e61d33e07051ce42.json @@ -0,0 +1,56 @@ +{ + "db_name": "PostgreSQL", + "query": "\n INSERT INTO projects (id, organization_id, agent_id, name, slug)\n VALUES ($1, $2, $3, $4, $5)\n RETURNING id, organization_id, agent_id, name, slug, created_at\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "organization_id", + "type_info": "Uuid" + }, + { + "ordinal": 2, + "name": "agent_id", + "type_info": "Uuid" + }, + { + "ordinal": 3, + "name": "name", + "type_info": "Text" + }, + { + "ordinal": 4, + "name": "slug", + "type_info": "Text" + }, + { + "ordinal": 5, + "name": "created_at", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [ + "Uuid", + "Uuid", + "Uuid", + "Text", + "Text" + ] + }, + "nullable": [ + false, + false, + false, + false, + false, + false + ] + }, + "hash": "04033eaad30ce63f08958fabfb5604aab131889512feeef9e61d33e07051ce42" +} diff --git a/lynx/.sqlx/query-04fa60cecac1c0c581ddd58cd4092413a6f8a3306c1a9e28921d410f35c243cc.json b/lynx/.sqlx/query-04fa60cecac1c0c581ddd58cd4092413a6f8a3306c1a9e28921d410f35c243cc.json new file mode 100644 index 0000000..77c0b0e --- /dev/null +++ b/lynx/.sqlx/query-04fa60cecac1c0c581ddd58cd4092413a6f8a3306c1a9e28921d410f35c243cc.json @@ -0,0 +1,23 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT agent_id FROM projects WHERE id = $1 AND organization_id = $2", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "agent_id", + "type_info": "Uuid" + } + ], + "parameters": { + "Left": [ + "Uuid", + "Uuid" + ] + }, + "nullable": [ + false + ] + }, + "hash": "04fa60cecac1c0c581ddd58cd4092413a6f8a3306c1a9e28921d410f35c243cc" +} diff --git a/lynx/.sqlx/query-05755ff8b74059273802f4586f7f8b640c6340e55407e4a15cce8d744557e7ad.json b/lynx/.sqlx/query-05755ff8b74059273802f4586f7f8b640c6340e55407e4a15cce8d744557e7ad.json new file mode 100644 index 0000000..8de728b --- /dev/null +++ b/lynx/.sqlx/query-05755ff8b74059273802f4586f7f8b640c6340e55407e4a15cce8d744557e7ad.json @@ -0,0 +1,15 @@ +{ + "db_name": "PostgreSQL", + "query": "DELETE FROM role_permissions WHERE role_id = $1 AND permission_id = $2", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid", + "Uuid" + ] + }, + "nullable": [] + }, + "hash": "05755ff8b74059273802f4586f7f8b640c6340e55407e4a15cce8d744557e7ad" +} diff --git a/lynx/.sqlx/query-07a0f39a809ce15570868cd4f1b02346ce233a4a6468b97c016bd52f4da349cb.json b/lynx/.sqlx/query-07a0f39a809ce15570868cd4f1b02346ce233a4a6468b97c016bd52f4da349cb.json new file mode 100644 index 0000000..32a1415 --- /dev/null +++ b/lynx/.sqlx/query-07a0f39a809ce15570868cd4f1b02346ce233a4a6468b97c016bd52f4da349cb.json @@ -0,0 +1,34 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT wg_ip, api_port, status FROM agents WHERE id = $1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "wg_ip", + "type_info": "Text" + }, + { + "ordinal": 1, + "name": "api_port", + "type_info": "Int4" + }, + { + "ordinal": 2, + "name": "status", + "type_info": "Text" + } + ], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [ + false, + false, + false + ] + }, + "hash": "07a0f39a809ce15570868cd4f1b02346ce233a4a6468b97c016bd52f4da349cb" +} diff --git a/lynx/.sqlx/query-07bfb9d7ecdb22c8e2e0d94b66f3ec87a4499b4895a586ae8cb4f70d2d23b216.json b/lynx/.sqlx/query-07bfb9d7ecdb22c8e2e0d94b66f3ec87a4499b4895a586ae8cb4f70d2d23b216.json new file mode 100644 index 0000000..c0d30dc --- /dev/null +++ b/lynx/.sqlx/query-07bfb9d7ecdb22c8e2e0d94b66f3ec87a4499b4895a586ae8cb4f70d2d23b216.json @@ -0,0 +1,23 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO data_plane_tunnels\n (id, project_id, agent_a_id, agent_b_id,\n agent_a_pubkey, agent_b_pubkey, agent_a_wg_ip, agent_b_wg_ip,\n wg_port, replica_count, status)\n VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,'pending')", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid", + "Uuid", + "Uuid", + "Uuid", + "Text", + "Text", + "Text", + "Text", + "Int4", + "Int4" + ] + }, + "nullable": [] + }, + "hash": "07bfb9d7ecdb22c8e2e0d94b66f3ec87a4499b4895a586ae8cb4f70d2d23b216" +} diff --git a/lynx/.sqlx/query-0d2e54f22a7f5179e7faa3a14dfc052aa3877a3dca544d527becfddcb610bbe1.json b/lynx/.sqlx/query-0d2e54f22a7f5179e7faa3a14dfc052aa3877a3dca544d527becfddcb610bbe1.json new file mode 100644 index 0000000..04434c0 --- /dev/null +++ b/lynx/.sqlx/query-0d2e54f22a7f5179e7faa3a14dfc052aa3877a3dca544d527becfddcb610bbe1.json @@ -0,0 +1,116 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT id, scope, agent_id, kind, port, port_end, protocol,\n ip_list, ip_version, rate_per_min, description,\n priority, enabled, direction, created_by, created_at, updated_at\n FROM nftables_rules\n WHERE scope = 'global' AND enabled = true\n ORDER BY priority ASC\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "scope", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "agent_id", + "type_info": "Uuid" + }, + { + "ordinal": 3, + "name": "kind", + "type_info": "Text" + }, + { + "ordinal": 4, + "name": "port", + "type_info": "Int4" + }, + { + "ordinal": 5, + "name": "port_end", + "type_info": "Int4" + }, + { + "ordinal": 6, + "name": "protocol", + "type_info": "Text" + }, + { + "ordinal": 7, + "name": "ip_list", + "type_info": "TextArray" + }, + { + "ordinal": 8, + "name": "ip_version", + "type_info": "Text" + }, + { + "ordinal": 9, + "name": "rate_per_min", + "type_info": "Int4" + }, + { + "ordinal": 10, + "name": "description", + "type_info": "Text" + }, + { + "ordinal": 11, + "name": "priority", + "type_info": "Int4" + }, + { + "ordinal": 12, + "name": "enabled", + "type_info": "Bool" + }, + { + "ordinal": 13, + "name": "direction", + "type_info": "Text" + }, + { + "ordinal": 14, + "name": "created_by", + "type_info": "Uuid" + }, + { + "ordinal": 15, + "name": "created_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 16, + "name": "updated_at", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + false, + false, + true, + false, + true, + true, + true, + false, + false, + true, + true, + false, + false, + false, + true, + false, + false + ] + }, + "hash": "0d2e54f22a7f5179e7faa3a14dfc052aa3877a3dca544d527becfddcb610bbe1" +} diff --git a/lynx/.sqlx/query-0d2ef2af6dd300873ac4c3ef7c79c4a3bb1758839f0ca921a87a99c97e800dea.json b/lynx/.sqlx/query-0d2ef2af6dd300873ac4c3ef7c79c4a3bb1758839f0ca921a87a99c97e800dea.json new file mode 100644 index 0000000..ba9791f --- /dev/null +++ b/lynx/.sqlx/query-0d2ef2af6dd300873ac4c3ef7c79c4a3bb1758839f0ca921a87a99c97e800dea.json @@ -0,0 +1,38 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT id, wg_pubkey, wg_ip::text AS wg_ip, api_port FROM agents WHERE status = 'online'", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "wg_pubkey", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "wg_ip", + "type_info": "Text" + }, + { + "ordinal": 3, + "name": "api_port", + "type_info": "Int4" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + false, + false, + false, + false + ] + }, + "hash": "0d2ef2af6dd300873ac4c3ef7c79c4a3bb1758839f0ca921a87a99c97e800dea" +} diff --git a/lynx/.sqlx/query-0d42b40bf313890d096a7ba1cd15e022a898ba6d9f1cd8a5ec64e43aa8c600f8.json b/lynx/.sqlx/query-0d42b40bf313890d096a7ba1cd15e022a898ba6d9f1cd8a5ec64e43aa8c600f8.json new file mode 100644 index 0000000..6f2ea35 --- /dev/null +++ b/lynx/.sqlx/query-0d42b40bf313890d096a7ba1cd15e022a898ba6d9f1cd8a5ec64e43aa8c600f8.json @@ -0,0 +1,17 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE agents SET status=$1, last_heartbeat=NOW(), version=$2, arch=$3 WHERE id=$4", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Text", + "Text", + "Text", + "Uuid" + ] + }, + "nullable": [] + }, + "hash": "0d42b40bf313890d096a7ba1cd15e022a898ba6d9f1cd8a5ec64e43aa8c600f8" +} diff --git a/lynx/.sqlx/query-0fc0009754fec1a23f7a214600967bc2e193c34494cc6e237cc1ee2e906ffe6d.json b/lynx/.sqlx/query-0fc0009754fec1a23f7a214600967bc2e193c34494cc6e237cc1ee2e906ffe6d.json new file mode 100644 index 0000000..17c5935 --- /dev/null +++ b/lynx/.sqlx/query-0fc0009754fec1a23f7a214600967bc2e193c34494cc6e237cc1ee2e906ffe6d.json @@ -0,0 +1,20 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT id FROM sessions WHERE expires_at > NOW()", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + false + ] + }, + "hash": "0fc0009754fec1a23f7a214600967bc2e193c34494cc6e237cc1ee2e906ffe6d" +} diff --git a/lynx/.sqlx/query-10eeee14e3f8d26f2a71272e8de60ecd89a96d564b5e1641ee585399bd4af3ff.json b/lynx/.sqlx/query-10eeee14e3f8d26f2a71272e8de60ecd89a96d564b5e1641ee585399bd4af3ff.json new file mode 100644 index 0000000..f09e746 --- /dev/null +++ b/lynx/.sqlx/query-10eeee14e3f8d26f2a71272e8de60ecd89a96d564b5e1641ee585399bd4af3ff.json @@ -0,0 +1,15 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO session_logs (id, session_id, reason) VALUES ($1, $2, 'jwt_rotation')", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid", + "Uuid" + ] + }, + "nullable": [] + }, + "hash": "10eeee14e3f8d26f2a71272e8de60ecd89a96d564b5e1641ee585399bd4af3ff" +} diff --git a/lynx/.sqlx/query-11e96cfd8c2736f13ce55975ea910dd68640f6f14e38a4b3342d514804e3de27.json b/lynx/.sqlx/query-11e96cfd8c2736f13ce55975ea910dd68640f6f14e38a4b3342d514804e3de27.json new file mode 100644 index 0000000..74a83ea --- /dev/null +++ b/lynx/.sqlx/query-11e96cfd8c2736f13ce55975ea910dd68640f6f14e38a4b3342d514804e3de27.json @@ -0,0 +1,14 @@ +{ + "db_name": "PostgreSQL", + "query": "DELETE FROM sessions WHERE id = $1", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [] + }, + "hash": "11e96cfd8c2736f13ce55975ea910dd68640f6f14e38a4b3342d514804e3de27" +} diff --git a/lynx/.sqlx/query-14e00756bed3d6e262de5aa2c1130655d6dc9ba1b8c4fc33ab0158861fb424cf.json b/lynx/.sqlx/query-14e00756bed3d6e262de5aa2c1130655d6dc9ba1b8c4fc33ab0158861fb424cf.json new file mode 100644 index 0000000..cacfc2d --- /dev/null +++ b/lynx/.sqlx/query-14e00756bed3d6e262de5aa2c1130655d6dc9ba1b8c4fc33ab0158861fb424cf.json @@ -0,0 +1,40 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT id, wg_ip, api_port, status FROM agents WHERE id = $1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "wg_ip", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "api_port", + "type_info": "Int4" + }, + { + "ordinal": 3, + "name": "status", + "type_info": "Text" + } + ], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [ + false, + false, + false, + false + ] + }, + "hash": "14e00756bed3d6e262de5aa2c1130655d6dc9ba1b8c4fc33ab0158861fb424cf" +} diff --git a/lynx/.sqlx/query-16b1751531b91212dc8da38f0a15cbb42d1be323ded00652547f942986479e33.json b/lynx/.sqlx/query-16b1751531b91212dc8da38f0a15cbb42d1be323ded00652547f942986479e33.json new file mode 100644 index 0000000..b23f30e --- /dev/null +++ b/lynx/.sqlx/query-16b1751531b91212dc8da38f0a15cbb42d1be323ded00652547f942986479e33.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "DELETE FROM nftables_rules WHERE id = $1 AND scope = 'global' RETURNING id", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + } + ], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [ + false + ] + }, + "hash": "16b1751531b91212dc8da38f0a15cbb42d1be323ded00652547f942986479e33" +} diff --git a/lynx/.sqlx/query-16ca089285971b7cf870f6e95ac91a0785d96760b72ff6f93dad5d063f73879b.json b/lynx/.sqlx/query-16ca089285971b7cf870f6e95ac91a0785d96760b72ff6f93dad5d063f73879b.json new file mode 100644 index 0000000..31a7294 --- /dev/null +++ b/lynx/.sqlx/query-16ca089285971b7cf870f6e95ac91a0785d96760b72ff6f93dad5d063f73879b.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT 1 FROM agents WHERE id = $1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "?column?", + "type_info": "Int4" + } + ], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [ + null + ] + }, + "hash": "16ca089285971b7cf870f6e95ac91a0785d96760b72ff6f93dad5d063f73879b" +} diff --git a/lynx/.sqlx/query-1840c87d613bff048d571a40be984385dce825047bf3dfb2c3e6022e8d05e186.json b/lynx/.sqlx/query-1840c87d613bff048d571a40be984385dce825047bf3dfb2c3e6022e8d05e186.json new file mode 100644 index 0000000..e9dfa0e --- /dev/null +++ b/lynx/.sqlx/query-1840c87d613bff048d571a40be984385dce825047bf3dfb2c3e6022e8d05e186.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT EXISTS(\n SELECT 1 FROM user_roles ur\n JOIN role_permissions rp ON rp.role_id = ur.role_id\n JOIN permissions p ON p.id = rp.permission_id\n WHERE ur.user_id = $1 AND p.key = '*:*'\n ) AS \"exists!\"", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "exists!", + "type_info": "Bool" + } + ], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [ + null + ] + }, + "hash": "1840c87d613bff048d571a40be984385dce825047bf3dfb2c3e6022e8d05e186" +} diff --git a/lynx/.sqlx/query-189218f43aaa3e902500a8ac8fd22288348c4cdfc7ffc1cafea1a132a7a93cdb.json b/lynx/.sqlx/query-189218f43aaa3e902500a8ac8fd22288348c4cdfc7ffc1cafea1a132a7a93cdb.json new file mode 100644 index 0000000..7a3cfaa --- /dev/null +++ b/lynx/.sqlx/query-189218f43aaa3e902500a8ac8fd22288348c4cdfc7ffc1cafea1a132a7a93cdb.json @@ -0,0 +1,28 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT r.id, r.name FROM user_roles ur JOIN roles r ON r.id = ur.role_id WHERE ur.user_id = $1 ORDER BY r.name", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "name", + "type_info": "Text" + } + ], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [ + false, + false + ] + }, + "hash": "189218f43aaa3e902500a8ac8fd22288348c4cdfc7ffc1cafea1a132a7a93cdb" +} diff --git a/lynx/.sqlx/query-1973560da6ea391a751f974fb0f0530e18bfff3efe81ef8cb2a5a5613259d229.json b/lynx/.sqlx/query-1973560da6ea391a751f974fb0f0530e18bfff3efe81ef8cb2a5a5613259d229.json new file mode 100644 index 0000000..fb99bfb --- /dev/null +++ b/lynx/.sqlx/query-1973560da6ea391a751f974fb0f0530e18bfff3efe81ef8cb2a5a5613259d229.json @@ -0,0 +1,52 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT id, ip, user_agent, created_at, last_used_at, expires_at\n FROM sessions\n WHERE user_id = $1 AND expires_at > NOW()\n ORDER BY last_used_at DESC\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "ip", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "user_agent", + "type_info": "Text" + }, + { + "ordinal": 3, + "name": "created_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 4, + "name": "last_used_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 5, + "name": "expires_at", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [ + false, + false, + true, + false, + false, + false + ] + }, + "hash": "1973560da6ea391a751f974fb0f0530e18bfff3efe81ef8cb2a5a5613259d229" +} diff --git a/lynx/.sqlx/query-1a63477354912301499a0a1dc347c2dfc67068e56496d364d0bb1a79aaeeecb0.json b/lynx/.sqlx/query-1a63477354912301499a0a1dc347c2dfc67068e56496d364d0bb1a79aaeeecb0.json new file mode 100644 index 0000000..e5e8bc1 --- /dev/null +++ b/lynx/.sqlx/query-1a63477354912301499a0a1dc347c2dfc67068e56496d364d0bb1a79aaeeecb0.json @@ -0,0 +1,20 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT status FROM migration_state WHERE id = 1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "status", + "type_info": "Text" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + false + ] + }, + "hash": "1a63477354912301499a0a1dc347c2dfc67068e56496d364d0bb1a79aaeeecb0" +} diff --git a/lynx/.sqlx/query-1a81592075c7e595b54bbe73962fe67deb509826b890a399e24e6c7cd8a25539.json b/lynx/.sqlx/query-1a81592075c7e595b54bbe73962fe67deb509826b890a399e24e6c7cd8a25539.json new file mode 100644 index 0000000..4677101 --- /dev/null +++ b/lynx/.sqlx/query-1a81592075c7e595b54bbe73962fe67deb509826b890a399e24e6c7cd8a25539.json @@ -0,0 +1,14 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE migration_state\n SET status='preparing', role='target', migration_token_hash=$1,\n started_at=NOW(), updated_at=NOW()\n WHERE id=1", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [] + }, + "hash": "1a81592075c7e595b54bbe73962fe67deb509826b890a399e24e6c7cd8a25539" +} diff --git a/lynx/.sqlx/query-1a9cb381b103f7b8dc0cce6fb461fecd343164473c960e788092c987d1abda41.json b/lynx/.sqlx/query-1a9cb381b103f7b8dc0cce6fb461fecd343164473c960e788092c987d1abda41.json new file mode 100644 index 0000000..87cc117 --- /dev/null +++ b/lynx/.sqlx/query-1a9cb381b103f7b8dc0cce6fb461fecd343164473c960e788092c987d1abda41.json @@ -0,0 +1,26 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT id, name FROM roles ORDER BY name", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "name", + "type_info": "Text" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + false, + false + ] + }, + "hash": "1a9cb381b103f7b8dc0cce6fb461fecd343164473c960e788092c987d1abda41" +} diff --git a/lynx/.sqlx/query-1cdf405ddf1eab2395fa7f406063b471cf58e07b32e008da87a8083361823bf6.json b/lynx/.sqlx/query-1cdf405ddf1eab2395fa7f406063b471cf58e07b32e008da87a8083361823bf6.json new file mode 100644 index 0000000..67b99b1 --- /dev/null +++ b/lynx/.sqlx/query-1cdf405ddf1eab2395fa7f406063b471cf58e07b32e008da87a8083361823bf6.json @@ -0,0 +1,14 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE users SET force_password_change = TRUE WHERE id = $1", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [] + }, + "hash": "1cdf405ddf1eab2395fa7f406063b471cf58e07b32e008da87a8083361823bf6" +} diff --git a/lynx/.sqlx/query-1d9739f9d79e70530c9337aec2f5c6756157de146062cee04c9c9aa2bb332818.json b/lynx/.sqlx/query-1d9739f9d79e70530c9337aec2f5c6756157de146062cee04c9c9aa2bb332818.json new file mode 100644 index 0000000..7aef988 --- /dev/null +++ b/lynx/.sqlx/query-1d9739f9d79e70530c9337aec2f5c6756157de146062cee04c9c9aa2bb332818.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT EXISTS(SELECT 1 FROM users WHERE username = $1)", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "exists", + "type_info": "Bool" + } + ], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [ + null + ] + }, + "hash": "1d9739f9d79e70530c9337aec2f5c6756157de146062cee04c9c9aa2bb332818" +} diff --git a/lynx/.sqlx/query-1e231d0b76bde0641c1779d2c270737d1e1de1d5731c0d19995352bc6b05a4d3.json b/lynx/.sqlx/query-1e231d0b76bde0641c1779d2c270737d1e1de1d5731c0d19995352bc6b05a4d3.json new file mode 100644 index 0000000..eed5f75 --- /dev/null +++ b/lynx/.sqlx/query-1e231d0b76bde0641c1779d2c270737d1e1de1d5731c0d19995352bc6b05a4d3.json @@ -0,0 +1,28 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT theme, locale FROM user_preferences WHERE user_id = $1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "theme", + "type_info": "Text" + }, + { + "ordinal": 1, + "name": "locale", + "type_info": "Text" + } + ], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [ + false, + false + ] + }, + "hash": "1e231d0b76bde0641c1779d2c270737d1e1de1d5731c0d19995352bc6b05a4d3" +} diff --git a/lynx/.sqlx/query-1f09b98d0fd53bb773b3b52af62db3d9e9c3bf98cdee3707981914c9198f244e.json b/lynx/.sqlx/query-1f09b98d0fd53bb773b3b52af62db3d9e9c3bf98cdee3707981914c9198f244e.json new file mode 100644 index 0000000..d58f1b0 --- /dev/null +++ b/lynx/.sqlx/query-1f09b98d0fd53bb773b3b52af62db3d9e9c3bf98cdee3707981914c9198f244e.json @@ -0,0 +1,14 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE domain_config SET domain=$1, status='pending', error_message=NULL, updated_at=NOW() WHERE id=1", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [] + }, + "hash": "1f09b98d0fd53bb773b3b52af62db3d9e9c3bf98cdee3707981914c9198f244e" +} diff --git a/lynx/.sqlx/query-1f5b0342ef80f28dd38c3fe69f0211f43fc7a7d208aae700a1972da82b7b6eed.json b/lynx/.sqlx/query-1f5b0342ef80f28dd38c3fe69f0211f43fc7a7d208aae700a1972da82b7b6eed.json new file mode 100644 index 0000000..6fe61db --- /dev/null +++ b/lynx/.sqlx/query-1f5b0342ef80f28dd38c3fe69f0211f43fc7a7d208aae700a1972da82b7b6eed.json @@ -0,0 +1,15 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO session_logs (id, session_id, reason) VALUES ($1, $2, 'mass_logout')", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid", + "Uuid" + ] + }, + "nullable": [] + }, + "hash": "1f5b0342ef80f28dd38c3fe69f0211f43fc7a7d208aae700a1972da82b7b6eed" +} diff --git a/lynx/.sqlx/query-204b794f714f283b07b6fe54b9005f24f8e9c352176ee693a2a5d296ecfbce29.json b/lynx/.sqlx/query-204b794f714f283b07b6fe54b9005f24f8e9c352176ee693a2a5d296ecfbce29.json new file mode 100644 index 0000000..454dc54 --- /dev/null +++ b/lynx/.sqlx/query-204b794f714f283b07b6fe54b9005f24f8e9c352176ee693a2a5d296ecfbce29.json @@ -0,0 +1,26 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT id, key FROM permissions ORDER BY key", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "key", + "type_info": "Text" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + false, + false + ] + }, + "hash": "204b794f714f283b07b6fe54b9005f24f8e9c352176ee693a2a5d296ecfbce29" +} diff --git a/lynx/.sqlx/query-211aa911356904f3ec992e00595cf624be631894dcb2c75755c550421e067a40.json b/lynx/.sqlx/query-211aa911356904f3ec992e00595cf624be631894dcb2c75755c550421e067a40.json new file mode 100644 index 0000000..29d53d8 --- /dev/null +++ b/lynx/.sqlx/query-211aa911356904f3ec992e00595cf624be631894dcb2c75755c550421e067a40.json @@ -0,0 +1,20 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT id FROM permissions WHERE key = '*:*'", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + false + ] + }, + "hash": "211aa911356904f3ec992e00595cf624be631894dcb2c75755c550421e067a40" +} diff --git a/lynx/.sqlx/query-21af8cca164cad6378377f035ec42e2cac73a91cb0cb36ff4c40b6ccc85e15ae.json b/lynx/.sqlx/query-21af8cca164cad6378377f035ec42e2cac73a91cb0cb36ff4c40b6ccc85e15ae.json new file mode 100644 index 0000000..0bcb4f7 --- /dev/null +++ b/lynx/.sqlx/query-21af8cca164cad6378377f035ec42e2cac73a91cb0cb36ff4c40b6ccc85e15ae.json @@ -0,0 +1,12 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE migration_state SET status='completed', completed_at=NOW(), updated_at=NOW() WHERE id=1", + "describe": { + "columns": [], + "parameters": { + "Left": [] + }, + "nullable": [] + }, + "hash": "21af8cca164cad6378377f035ec42e2cac73a91cb0cb36ff4c40b6ccc85e15ae" +} diff --git a/lynx/.sqlx/query-265bd5e210dd30803c2bbfb22d2504e69b17ebf5a314bdb9fb64c15e21af1cad.json b/lynx/.sqlx/query-265bd5e210dd30803c2bbfb22d2504e69b17ebf5a314bdb9fb64c15e21af1cad.json new file mode 100644 index 0000000..ec3a4a9 --- /dev/null +++ b/lynx/.sqlx/query-265bd5e210dd30803c2bbfb22d2504e69b17ebf5a314bdb9fb64c15e21af1cad.json @@ -0,0 +1,129 @@ +{ + "db_name": "PostgreSQL", + "query": "\n INSERT INTO nftables_rules\n (id, scope, agent_id, kind, port, port_end, protocol, ip_list, ip_version,\n rate_per_min, description, priority, direction, created_by)\n VALUES ($1, 'global', NULL, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)\n RETURNING id, scope, agent_id, kind, port, port_end, protocol,\n ip_list, ip_version, rate_per_min, description,\n priority, enabled, direction, created_by, created_at, updated_at\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "scope", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "agent_id", + "type_info": "Uuid" + }, + { + "ordinal": 3, + "name": "kind", + "type_info": "Text" + }, + { + "ordinal": 4, + "name": "port", + "type_info": "Int4" + }, + { + "ordinal": 5, + "name": "port_end", + "type_info": "Int4" + }, + { + "ordinal": 6, + "name": "protocol", + "type_info": "Text" + }, + { + "ordinal": 7, + "name": "ip_list", + "type_info": "TextArray" + }, + { + "ordinal": 8, + "name": "ip_version", + "type_info": "Text" + }, + { + "ordinal": 9, + "name": "rate_per_min", + "type_info": "Int4" + }, + { + "ordinal": 10, + "name": "description", + "type_info": "Text" + }, + { + "ordinal": 11, + "name": "priority", + "type_info": "Int4" + }, + { + "ordinal": 12, + "name": "enabled", + "type_info": "Bool" + }, + { + "ordinal": 13, + "name": "direction", + "type_info": "Text" + }, + { + "ordinal": 14, + "name": "created_by", + "type_info": "Uuid" + }, + { + "ordinal": 15, + "name": "created_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 16, + "name": "updated_at", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [ + "Uuid", + "Text", + "Int4", + "Int4", + "Text", + "TextArray", + "Text", + "Int4", + "Text", + "Int4", + "Text", + "Uuid" + ] + }, + "nullable": [ + false, + false, + true, + false, + true, + true, + true, + false, + false, + true, + true, + false, + false, + false, + true, + false, + false + ] + }, + "hash": "265bd5e210dd30803c2bbfb22d2504e69b17ebf5a314bdb9fb64c15e21af1cad" +} diff --git a/lynx/.sqlx/query-26da9efd448c979af02634d0ac3b7280ade8af8dea96a1237732317c32643c35.json b/lynx/.sqlx/query-26da9efd448c979af02634d0ac3b7280ade8af8dea96a1237732317c32643c35.json new file mode 100644 index 0000000..da6b91c --- /dev/null +++ b/lynx/.sqlx/query-26da9efd448c979af02634d0ac3b7280ade8af8dea96a1237732317c32643c35.json @@ -0,0 +1,17 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO agent_events (id, agent_id, event, detail) VALUES ($1, $2, $3, $4)", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid", + "Uuid", + "Text", + "Text" + ] + }, + "nullable": [] + }, + "hash": "26da9efd448c979af02634d0ac3b7280ade8af8dea96a1237732317c32643c35" +} diff --git a/lynx/.sqlx/query-27170bba17f0a3741d6675957c6e6d25ecec938882b5221440a0797c67eb3c36.json b/lynx/.sqlx/query-27170bba17f0a3741d6675957c6e6d25ecec938882b5221440a0797c67eb3c36.json new file mode 100644 index 0000000..98b306a --- /dev/null +++ b/lynx/.sqlx/query-27170bba17f0a3741d6675957c6e6d25ecec938882b5221440a0797c67eb3c36.json @@ -0,0 +1,15 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO agent_events (id, agent_id, event, detail) VALUES ($1, $2, 'disconnected', NULL)", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid", + "Uuid" + ] + }, + "nullable": [] + }, + "hash": "27170bba17f0a3741d6675957c6e6d25ecec938882b5221440a0797c67eb3c36" +} diff --git a/lynx/.sqlx/query-28b24383bd086639f396e2bb59ae7620ef22bc6083c74a810ba4017c92ba7fd3.json b/lynx/.sqlx/query-28b24383bd086639f396e2bb59ae7620ef22bc6083c74a810ba4017c92ba7fd3.json new file mode 100644 index 0000000..608e27a --- /dev/null +++ b/lynx/.sqlx/query-28b24383bd086639f396e2bb59ae7620ef22bc6083c74a810ba4017c92ba7fd3.json @@ -0,0 +1,17 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO agent_events (id, agent_id, event, detail, created_at) VALUES ($1, $2, $3, $4, NOW())", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid", + "Uuid", + "Text", + "Text" + ] + }, + "nullable": [] + }, + "hash": "28b24383bd086639f396e2bb59ae7620ef22bc6083c74a810ba4017c92ba7fd3" +} diff --git a/lynx/.sqlx/query-2927ed8c7aee5fc5b89fc57d3560d82f6f10ed965f9f30bd872dfb94e7377080.json b/lynx/.sqlx/query-2927ed8c7aee5fc5b89fc57d3560d82f6f10ed965f9f30bd872dfb94e7377080.json new file mode 100644 index 0000000..2ac5dd1 --- /dev/null +++ b/lynx/.sqlx/query-2927ed8c7aee5fc5b89fc57d3560d82f6f10ed965f9f30bd872dfb94e7377080.json @@ -0,0 +1,12 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE migration_state SET status='aborted', updated_at=NOW() WHERE id=1", + "describe": { + "columns": [], + "parameters": { + "Left": [] + }, + "nullable": [] + }, + "hash": "2927ed8c7aee5fc5b89fc57d3560d82f6f10ed965f9f30bd872dfb94e7377080" +} diff --git a/lynx/.sqlx/query-2a4fdddd31c59d96ebce2d15307c1b681ea95e5ccfd77691b07aad740f17b932.json b/lynx/.sqlx/query-2a4fdddd31c59d96ebce2d15307c1b681ea95e5ccfd77691b07aad740f17b932.json new file mode 100644 index 0000000..7b80fd0 --- /dev/null +++ b/lynx/.sqlx/query-2a4fdddd31c59d96ebce2d15307c1b681ea95e5ccfd77691b07aad740f17b932.json @@ -0,0 +1,12 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE migration_state SET status='waiting_agents', updated_at=NOW() WHERE id=1", + "describe": { + "columns": [], + "parameters": { + "Left": [] + }, + "nullable": [] + }, + "hash": "2a4fdddd31c59d96ebce2d15307c1b681ea95e5ccfd77691b07aad740f17b932" +} diff --git a/lynx/.sqlx/query-2c30e55d43c427acce4c7f21d86435b7e4fcf5b0252d8b73f0692ff1e20b99af.json b/lynx/.sqlx/query-2c30e55d43c427acce4c7f21d86435b7e4fcf5b0252d8b73f0692ff1e20b99af.json new file mode 100644 index 0000000..c762891 --- /dev/null +++ b/lynx/.sqlx/query-2c30e55d43c427acce4c7f21d86435b7e4fcf5b0252d8b73f0692ff1e20b99af.json @@ -0,0 +1,14 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE data_plane_tunnels SET status='active', updated_at=NOW() WHERE id=$1", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [] + }, + "hash": "2c30e55d43c427acce4c7f21d86435b7e4fcf5b0252d8b73f0692ff1e20b99af" +} diff --git a/lynx/.sqlx/query-2d03c5c6a574aab90998a1b5635492733fcfbb71b7846eadd50bf8819b0fdfe8.json b/lynx/.sqlx/query-2d03c5c6a574aab90998a1b5635492733fcfbb71b7846eadd50bf8819b0fdfe8.json new file mode 100644 index 0000000..91d4c06 --- /dev/null +++ b/lynx/.sqlx/query-2d03c5c6a574aab90998a1b5635492733fcfbb71b7846eadd50bf8819b0fdfe8.json @@ -0,0 +1,44 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT id, triggered_by, reason, scope, created_at\n FROM rotation_log\n ORDER BY created_at DESC\n LIMIT 50\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "triggered_by", + "type_info": "Uuid" + }, + { + "ordinal": 2, + "name": "reason", + "type_info": "Text" + }, + { + "ordinal": 3, + "name": "scope", + "type_info": "Text" + }, + { + "ordinal": 4, + "name": "created_at", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + false, + true, + false, + false, + false + ] + }, + "hash": "2d03c5c6a574aab90998a1b5635492733fcfbb71b7846eadd50bf8819b0fdfe8" +} diff --git a/lynx/.sqlx/query-31c8b54aba4d9bd08ea2188597246120e504efae5eb87989b47a274b76dcacfb.json b/lynx/.sqlx/query-31c8b54aba4d9bd08ea2188597246120e504efae5eb87989b47a274b76dcacfb.json new file mode 100644 index 0000000..6436b7c --- /dev/null +++ b/lynx/.sqlx/query-31c8b54aba4d9bd08ea2188597246120e504efae5eb87989b47a274b76dcacfb.json @@ -0,0 +1,14 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE agents SET status='online', last_heartbeat=NOW() WHERE id=$1", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [] + }, + "hash": "31c8b54aba4d9bd08ea2188597246120e504efae5eb87989b47a274b76dcacfb" +} diff --git a/lynx/.sqlx/query-331133cf9c129cf4e8c7e12b7ea78d891846595d320ca9faf858df753b995681.json b/lynx/.sqlx/query-331133cf9c129cf4e8c7e12b7ea78d891846595d320ca9faf858df753b995681.json new file mode 100644 index 0000000..09d620f --- /dev/null +++ b/lynx/.sqlx/query-331133cf9c129cf4e8c7e12b7ea78d891846595d320ca9faf858df753b995681.json @@ -0,0 +1,32 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT id, wg_ip::text AS wg_ip, api_port FROM agents WHERE is_local_agent = true LIMIT 1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "wg_ip", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "api_port", + "type_info": "Int4" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + false, + false, + false + ] + }, + "hash": "331133cf9c129cf4e8c7e12b7ea78d891846595d320ca9faf858df753b995681" +} diff --git a/lynx/.sqlx/query-377a98cd59592dbf445eaae12d4ac913d9e837876c86deafa56cfc4816d648b1.json b/lynx/.sqlx/query-377a98cd59592dbf445eaae12d4ac913d9e837876c86deafa56cfc4816d648b1.json new file mode 100644 index 0000000..3729815 --- /dev/null +++ b/lynx/.sqlx/query-377a98cd59592dbf445eaae12d4ac913d9e837876c86deafa56cfc4816d648b1.json @@ -0,0 +1,50 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT id, name, status, wg_ip, version, last_heartbeat FROM agents ORDER BY created_at ASC", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "name", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "status", + "type_info": "Text" + }, + { + "ordinal": 3, + "name": "wg_ip", + "type_info": "Text" + }, + { + "ordinal": 4, + "name": "version", + "type_info": "Text" + }, + { + "ordinal": 5, + "name": "last_heartbeat", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + false, + false, + false, + false, + true, + true + ] + }, + "hash": "377a98cd59592dbf445eaae12d4ac913d9e837876c86deafa56cfc4816d648b1" +} diff --git a/lynx/.sqlx/query-37839ae3a690f52077fe56fd012a361929bfc0d90c3e13f387ec5bb7c8a25fcb.json b/lynx/.sqlx/query-37839ae3a690f52077fe56fd012a361929bfc0d90c3e13f387ec5bb7c8a25fcb.json new file mode 100644 index 0000000..89b8ba7 --- /dev/null +++ b/lynx/.sqlx/query-37839ae3a690f52077fe56fd012a361929bfc0d90c3e13f387ec5bb7c8a25fcb.json @@ -0,0 +1,118 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT id, scope, agent_id, kind, port, port_end, protocol,\n ip_list, ip_version, rate_per_min, description,\n priority, enabled, direction, created_by, created_at, updated_at\n FROM nftables_rules\n WHERE scope = 'local' AND agent_id = $1 AND enabled = true\n ORDER BY priority ASC\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "scope", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "agent_id", + "type_info": "Uuid" + }, + { + "ordinal": 3, + "name": "kind", + "type_info": "Text" + }, + { + "ordinal": 4, + "name": "port", + "type_info": "Int4" + }, + { + "ordinal": 5, + "name": "port_end", + "type_info": "Int4" + }, + { + "ordinal": 6, + "name": "protocol", + "type_info": "Text" + }, + { + "ordinal": 7, + "name": "ip_list", + "type_info": "TextArray" + }, + { + "ordinal": 8, + "name": "ip_version", + "type_info": "Text" + }, + { + "ordinal": 9, + "name": "rate_per_min", + "type_info": "Int4" + }, + { + "ordinal": 10, + "name": "description", + "type_info": "Text" + }, + { + "ordinal": 11, + "name": "priority", + "type_info": "Int4" + }, + { + "ordinal": 12, + "name": "enabled", + "type_info": "Bool" + }, + { + "ordinal": 13, + "name": "direction", + "type_info": "Text" + }, + { + "ordinal": 14, + "name": "created_by", + "type_info": "Uuid" + }, + { + "ordinal": 15, + "name": "created_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 16, + "name": "updated_at", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [ + false, + false, + true, + false, + true, + true, + true, + false, + false, + true, + true, + false, + false, + false, + true, + false, + false + ] + }, + "hash": "37839ae3a690f52077fe56fd012a361929bfc0d90c3e13f387ec5bb7c8a25fcb" +} diff --git a/lynx/.sqlx/query-37d4ef396e93339202bc64ba7eefbea379b9978eedd006403e090600d3f4680f.json b/lynx/.sqlx/query-37d4ef396e93339202bc64ba7eefbea379b9978eedd006403e090600d3f4680f.json new file mode 100644 index 0000000..4e2bb8a --- /dev/null +++ b/lynx/.sqlx/query-37d4ef396e93339202bc64ba7eefbea379b9978eedd006403e090600d3f4680f.json @@ -0,0 +1,23 @@ +{ + "db_name": "PostgreSQL", + "query": "\n INSERT INTO audit_log (\n id, agent_id, organization_id, user_id, command_type,\n result, error, previous_hash, entry_hash, created_at\n ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)\n ON CONFLICT (id) DO NOTHING\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid", + "Uuid", + "Uuid", + "Uuid", + "Text", + "Text", + "Text", + "Text", + "Text", + "Timestamptz" + ] + }, + "nullable": [] + }, + "hash": "37d4ef396e93339202bc64ba7eefbea379b9978eedd006403e090600d3f4680f" +} diff --git a/lynx/.sqlx/query-38331e94fd07618f293c2174459445b5edaa81cf2a527abcbd8650360c261927.json b/lynx/.sqlx/query-38331e94fd07618f293c2174459445b5edaa81cf2a527abcbd8650360c261927.json new file mode 100644 index 0000000..4c7e194 --- /dev/null +++ b/lynx/.sqlx/query-38331e94fd07618f293c2174459445b5edaa81cf2a527abcbd8650360c261927.json @@ -0,0 +1,20 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT domain FROM domain_config WHERE id = 1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "domain", + "type_info": "Text" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + true + ] + }, + "hash": "38331e94fd07618f293c2174459445b5edaa81cf2a527abcbd8650360c261927" +} diff --git a/lynx/.sqlx/query-388de60f218494e05573b58846255397c6993a77ebdc93fd0c92394b3655012e.json b/lynx/.sqlx/query-388de60f218494e05573b58846255397c6993a77ebdc93fd0c92394b3655012e.json new file mode 100644 index 0000000..84694ed --- /dev/null +++ b/lynx/.sqlx/query-388de60f218494e05573b58846255397c6993a77ebdc93fd0c92394b3655012e.json @@ -0,0 +1,16 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO user_preferences (user_id, theme, locale)\n VALUES ($1, COALESCE($2, 'system'), COALESCE($3, 'en'))\n ON CONFLICT (user_id) DO UPDATE SET\n theme = COALESCE($2, user_preferences.theme),\n locale = COALESCE($3, user_preferences.locale),\n updated_at = NOW()", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid", + "Text", + "Text" + ] + }, + "nullable": [] + }, + "hash": "388de60f218494e05573b58846255397c6993a77ebdc93fd0c92394b3655012e" +} diff --git a/lynx/.sqlx/query-3a9e69b7c9cae72e08c7fb8094b086789094599f575a6d27b2ad7e15cb71eac6.json b/lynx/.sqlx/query-3a9e69b7c9cae72e08c7fb8094b086789094599f575a6d27b2ad7e15cb71eac6.json new file mode 100644 index 0000000..dac9a44 --- /dev/null +++ b/lynx/.sqlx/query-3a9e69b7c9cae72e08c7fb8094b086789094599f575a6d27b2ad7e15cb71eac6.json @@ -0,0 +1,34 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT wg_ip, api_port, status FROM agents WHERE id=$1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "wg_ip", + "type_info": "Text" + }, + { + "ordinal": 1, + "name": "api_port", + "type_info": "Int4" + }, + { + "ordinal": 2, + "name": "status", + "type_info": "Text" + } + ], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [ + false, + false, + false + ] + }, + "hash": "3a9e69b7c9cae72e08c7fb8094b086789094599f575a6d27b2ad7e15cb71eac6" +} diff --git a/lynx/.sqlx/query-3c8140984103bed6d663024e9c846250ed9077ff59f33e4c285c21ffb5d3c031.json b/lynx/.sqlx/query-3c8140984103bed6d663024e9c846250ed9077ff59f33e4c285c21ffb5d3c031.json new file mode 100644 index 0000000..ba5e1dd --- /dev/null +++ b/lynx/.sqlx/query-3c8140984103bed6d663024e9c846250ed9077ff59f33e4c285c21ffb5d3c031.json @@ -0,0 +1,18 @@ +{ + "db_name": "PostgreSQL", + "query": "\n INSERT INTO white_label (id, company_name, logo_url, primary_color, secondary_color, accent_color, updated_at)\n VALUES (1,\n COALESCE($1, 'Lynx'),\n $2,\n COALESCE($3, '#0f172a'),\n COALESCE($4, '#38bdf8'),\n COALESCE($5, '#6366f1'),\n NOW()\n )\n ON CONFLICT (id) DO UPDATE SET\n company_name = COALESCE($1, white_label.company_name),\n logo_url = COALESCE($2, white_label.logo_url),\n primary_color = COALESCE($3, white_label.primary_color),\n secondary_color = COALESCE($4, white_label.secondary_color),\n accent_color = COALESCE($5, white_label.accent_color),\n updated_at = NOW()\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Text", + "Text", + "Text", + "Text", + "Text" + ] + }, + "nullable": [] + }, + "hash": "3c8140984103bed6d663024e9c846250ed9077ff59f33e4c285c21ffb5d3c031" +} diff --git a/lynx/.sqlx/query-3cd7469d8dd28de161b63eebba41c039d9f3eaf3267879544f8f6fb615a0fafa.json b/lynx/.sqlx/query-3cd7469d8dd28de161b63eebba41c039d9f3eaf3267879544f8f6fb615a0fafa.json new file mode 100644 index 0000000..369c60e --- /dev/null +++ b/lynx/.sqlx/query-3cd7469d8dd28de161b63eebba41c039d9f3eaf3267879544f8f6fb615a0fafa.json @@ -0,0 +1,15 @@ +{ + "db_name": "PostgreSQL", + "query": "DELETE FROM organizations WHERE id = $1 AND owner_id = $2", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid", + "Uuid" + ] + }, + "nullable": [] + }, + "hash": "3cd7469d8dd28de161b63eebba41c039d9f3eaf3267879544f8f6fb615a0fafa" +} diff --git a/lynx/.sqlx/query-3e6e8301c56e857fab3918b31b9e73133f690d14743015bffeaf4c26c63fbb94.json b/lynx/.sqlx/query-3e6e8301c56e857fab3918b31b9e73133f690d14743015bffeaf4c26c63fbb94.json new file mode 100644 index 0000000..e4ef779 --- /dev/null +++ b/lynx/.sqlx/query-3e6e8301c56e857fab3918b31b9e73133f690d14743015bffeaf4c26c63fbb94.json @@ -0,0 +1,12 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE domain_config SET port_19443_open=false, updated_at=NOW() WHERE id=1", + "describe": { + "columns": [], + "parameters": { + "Left": [] + }, + "nullable": [] + }, + "hash": "3e6e8301c56e857fab3918b31b9e73133f690d14743015bffeaf4c26c63fbb94" +} diff --git a/lynx/.sqlx/query-3f37bcaf4d27e480d7c46f9325a4262a37c02a062a804f08e4ff8a7e7c3c395c.json b/lynx/.sqlx/query-3f37bcaf4d27e480d7c46f9325a4262a37c02a062a804f08e4ff8a7e7c3c395c.json new file mode 100644 index 0000000..e781aa4 --- /dev/null +++ b/lynx/.sqlx/query-3f37bcaf4d27e480d7c46f9325a4262a37c02a062a804f08e4ff8a7e7c3c395c.json @@ -0,0 +1,17 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO user_roles (id, user_id, role_id, created_by) VALUES ($1, $2, $3, $4)\n ON CONFLICT (user_id, role_id) DO NOTHING", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid", + "Uuid", + "Uuid", + "Uuid" + ] + }, + "nullable": [] + }, + "hash": "3f37bcaf4d27e480d7c46f9325a4262a37c02a062a804f08e4ff8a7e7c3c395c" +} diff --git a/lynx/.sqlx/query-412b66ba62bd41ff74d08cc5075673d717f0fe9f7d40f8cdeef4956dc0a63318.json b/lynx/.sqlx/query-412b66ba62bd41ff74d08cc5075673d717f0fe9f7d40f8cdeef4956dc0a63318.json new file mode 100644 index 0000000..f3dd600 --- /dev/null +++ b/lynx/.sqlx/query-412b66ba62bd41ff74d08cc5075673d717f0fe9f7d40f8cdeef4956dc0a63318.json @@ -0,0 +1,34 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT id, wg_ip, api_port FROM agents WHERE id = $1 AND status = 'online'", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "wg_ip", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "api_port", + "type_info": "Int4" + } + ], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [ + false, + false, + false + ] + }, + "hash": "412b66ba62bd41ff74d08cc5075673d717f0fe9f7d40f8cdeef4956dc0a63318" +} diff --git a/lynx/.sqlx/query-463e3cb3cc41990e508d9159e6e4043629edcc6761ce8ccaddfafc51523b2991.json b/lynx/.sqlx/query-463e3cb3cc41990e508d9159e6e4043629edcc6761ce8ccaddfafc51523b2991.json new file mode 100644 index 0000000..6cb1b87 --- /dev/null +++ b/lynx/.sqlx/query-463e3cb3cc41990e508d9159e6e4043629edcc6761ce8ccaddfafc51523b2991.json @@ -0,0 +1,14 @@ +{ + "db_name": "PostgreSQL", + "query": "DELETE FROM roles WHERE id = $1", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [] + }, + "hash": "463e3cb3cc41990e508d9159e6e4043629edcc6761ce8ccaddfafc51523b2991" +} diff --git a/lynx/.sqlx/query-476c825437be3dcacbe3fd880af94763f6c5e572fac927159c22449ee66e274b.json b/lynx/.sqlx/query-476c825437be3dcacbe3fd880af94763f6c5e572fac927159c22449ee66e274b.json new file mode 100644 index 0000000..927be5e --- /dev/null +++ b/lynx/.sqlx/query-476c825437be3dcacbe3fd880af94763f6c5e572fac927159c22449ee66e274b.json @@ -0,0 +1,15 @@ +{ + "db_name": "PostgreSQL", + "query": "DELETE FROM organization_members WHERE organization_id = $1 AND user_id = $2", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid", + "Uuid" + ] + }, + "nullable": [] + }, + "hash": "476c825437be3dcacbe3fd880af94763f6c5e572fac927159c22449ee66e274b" +} diff --git a/lynx/.sqlx/query-49ec05b4abcd978617b54d6b313d7b7e51a1cf18461b75eef85b1e53708e0ce1.json b/lynx/.sqlx/query-49ec05b4abcd978617b54d6b313d7b7e51a1cf18461b75eef85b1e53708e0ce1.json new file mode 100644 index 0000000..1ece34a --- /dev/null +++ b/lynx/.sqlx/query-49ec05b4abcd978617b54d6b313d7b7e51a1cf18461b75eef85b1e53708e0ce1.json @@ -0,0 +1,15 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO roles (id, name, created_by) VALUES ($1, 'Admin', $2)", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid", + "Uuid" + ] + }, + "nullable": [] + }, + "hash": "49ec05b4abcd978617b54d6b313d7b7e51a1cf18461b75eef85b1e53708e0ce1" +} diff --git a/lynx/.sqlx/query-4b080e91cd13b16c5820a9cabdaf1b6f00f7c660a43df4867ffb733bac029461.json b/lynx/.sqlx/query-4b080e91cd13b16c5820a9cabdaf1b6f00f7c660a43df4867ffb733bac029461.json new file mode 100644 index 0000000..666b775 --- /dev/null +++ b/lynx/.sqlx/query-4b080e91cd13b16c5820a9cabdaf1b6f00f7c660a43df4867ffb733bac029461.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT COUNT(*) FROM audit_log WHERE agent_id = $1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "count", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [ + null + ] + }, + "hash": "4b080e91cd13b16c5820a9cabdaf1b6f00f7c660a43df4867ffb733bac029461" +} diff --git a/lynx/.sqlx/query-4bf02ac6e05fb3a91e639d0b20c0f0c3c6b30e5c936e285d2b8c2ead4ec2a466.json b/lynx/.sqlx/query-4bf02ac6e05fb3a91e639d0b20c0f0c3c6b30e5c936e285d2b8c2ead4ec2a466.json new file mode 100644 index 0000000..45e8389 --- /dev/null +++ b/lynx/.sqlx/query-4bf02ac6e05fb3a91e639d0b20c0f0c3c6b30e5c936e285d2b8c2ead4ec2a466.json @@ -0,0 +1,16 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO session_logs (id, session_id, reason) VALUES ($1, $2, $3)", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid", + "Uuid", + "Text" + ] + }, + "nullable": [] + }, + "hash": "4bf02ac6e05fb3a91e639d0b20c0f0c3c6b30e5c936e285d2b8c2ead4ec2a466" +} diff --git a/lynx/.sqlx/query-4f572f12c9c07f76b598f346b3c926eb6e1a2cdc4f9d59b7211f54041ccafe7d.json b/lynx/.sqlx/query-4f572f12c9c07f76b598f346b3c926eb6e1a2cdc4f9d59b7211f54041ccafe7d.json new file mode 100644 index 0000000..d58f28b --- /dev/null +++ b/lynx/.sqlx/query-4f572f12c9c07f76b598f346b3c926eb6e1a2cdc4f9d59b7211f54041ccafe7d.json @@ -0,0 +1,15 @@ +{ + "db_name": "PostgreSQL", + "query": "\n INSERT INTO global_rule_sync (rule_id, agent_id, synced_at)\n VALUES ($1, $2, NOW())\n ON CONFLICT (rule_id, agent_id) DO UPDATE SET synced_at = NOW()\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid", + "Uuid" + ] + }, + "nullable": [] + }, + "hash": "4f572f12c9c07f76b598f346b3c926eb6e1a2cdc4f9d59b7211f54041ccafe7d" +} diff --git a/lynx/.sqlx/query-4f863def6d4a9994282dd10a4fb845f09c114022f64afc980a6538c0f22bc1f8.json b/lynx/.sqlx/query-4f863def6d4a9994282dd10a4fb845f09c114022f64afc980a6538c0f22bc1f8.json new file mode 100644 index 0000000..fcc3f88 --- /dev/null +++ b/lynx/.sqlx/query-4f863def6d4a9994282dd10a4fb845f09c114022f64afc980a6538c0f22bc1f8.json @@ -0,0 +1,118 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT id, scope, agent_id, kind, port, port_end, protocol,\n ip_list, ip_version, rate_per_min, description,\n priority, enabled, direction, created_by, created_at, updated_at\n FROM nftables_rules\n WHERE scope = 'local' AND agent_id = $1\n ORDER BY priority ASC, created_at ASC\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "scope", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "agent_id", + "type_info": "Uuid" + }, + { + "ordinal": 3, + "name": "kind", + "type_info": "Text" + }, + { + "ordinal": 4, + "name": "port", + "type_info": "Int4" + }, + { + "ordinal": 5, + "name": "port_end", + "type_info": "Int4" + }, + { + "ordinal": 6, + "name": "protocol", + "type_info": "Text" + }, + { + "ordinal": 7, + "name": "ip_list", + "type_info": "TextArray" + }, + { + "ordinal": 8, + "name": "ip_version", + "type_info": "Text" + }, + { + "ordinal": 9, + "name": "rate_per_min", + "type_info": "Int4" + }, + { + "ordinal": 10, + "name": "description", + "type_info": "Text" + }, + { + "ordinal": 11, + "name": "priority", + "type_info": "Int4" + }, + { + "ordinal": 12, + "name": "enabled", + "type_info": "Bool" + }, + { + "ordinal": 13, + "name": "direction", + "type_info": "Text" + }, + { + "ordinal": 14, + "name": "created_by", + "type_info": "Uuid" + }, + { + "ordinal": 15, + "name": "created_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 16, + "name": "updated_at", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [ + false, + false, + true, + false, + true, + true, + true, + false, + false, + true, + true, + false, + false, + false, + true, + false, + false + ] + }, + "hash": "4f863def6d4a9994282dd10a4fb845f09c114022f64afc980a6538c0f22bc1f8" +} diff --git a/lynx/.sqlx/query-4fb5bb08cece498a68856a0e3009289b985ee3016743934dfeb3e17afe243d17.json b/lynx/.sqlx/query-4fb5bb08cece498a68856a0e3009289b985ee3016743934dfeb3e17afe243d17.json new file mode 100644 index 0000000..2bea0bf --- /dev/null +++ b/lynx/.sqlx/query-4fb5bb08cece498a68856a0e3009289b985ee3016743934dfeb3e17afe243d17.json @@ -0,0 +1,32 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT status, agents_total, agents_confirmed FROM migration_state WHERE id=1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "status", + "type_info": "Text" + }, + { + "ordinal": 1, + "name": "agents_total", + "type_info": "Int4" + }, + { + "ordinal": 2, + "name": "agents_confirmed", + "type_info": "Int4" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + false, + false, + false + ] + }, + "hash": "4fb5bb08cece498a68856a0e3009289b985ee3016743934dfeb3e17afe243d17" +} diff --git a/lynx/.sqlx/query-50293c2e54af11d4c2a553e29b671cef087a159c6ee7182d8ca929ecb748f3b7.json b/lynx/.sqlx/query-50293c2e54af11d4c2a553e29b671cef087a159c6ee7182d8ca929ecb748f3b7.json new file mode 100644 index 0000000..f62678a --- /dev/null +++ b/lynx/.sqlx/query-50293c2e54af11d4c2a553e29b671cef087a159c6ee7182d8ca929ecb748f3b7.json @@ -0,0 +1,14 @@ +{ + "db_name": "PostgreSQL", + "query": "DELETE FROM users WHERE id = $1", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [] + }, + "hash": "50293c2e54af11d4c2a553e29b671cef087a159c6ee7182d8ca929ecb748f3b7" +} diff --git a/lynx/.sqlx/query-50f8e32fc7290f7c69685700e89ea70d58725b3761a08ce1e26e5a5afca6549d.json b/lynx/.sqlx/query-50f8e32fc7290f7c69685700e89ea70d58725b3761a08ce1e26e5a5afca6549d.json new file mode 100644 index 0000000..fd029d7 --- /dev/null +++ b/lynx/.sqlx/query-50f8e32fc7290f7c69685700e89ea70d58725b3761a08ce1e26e5a5afca6549d.json @@ -0,0 +1,19 @@ +{ + "db_name": "PostgreSQL", + "query": "\n INSERT INTO update_log (id, triggered_by, version, channel, scope, agent_id, status)\n VALUES ($1, $2, $3, $4, 'agent', $5, $6)\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid", + "Uuid", + "Text", + "Text", + "Uuid", + "Text" + ] + }, + "nullable": [] + }, + "hash": "50f8e32fc7290f7c69685700e89ea70d58725b3761a08ce1e26e5a5afca6549d" +} diff --git a/lynx/.sqlx/query-5102fd11d51afd502b1490a7ab201325a108b99b20f00f325c5f578fe166d47a.json b/lynx/.sqlx/query-5102fd11d51afd502b1490a7ab201325a108b99b20f00f325c5f578fe166d47a.json new file mode 100644 index 0000000..d326149 --- /dev/null +++ b/lynx/.sqlx/query-5102fd11d51afd502b1490a7ab201325a108b99b20f00f325c5f578fe166d47a.json @@ -0,0 +1,83 @@ +{ + "db_name": "PostgreSQL", + "query": "\n INSERT INTO agents (id, name, wg_pubkey, wg_ip, wg_endpoint, api_port, sync_token_hash, is_local_agent)\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8)\n RETURNING id, name, wg_pubkey, wg_ip, wg_endpoint,\n api_port, status, version, last_heartbeat, created_at\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "name", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "wg_pubkey", + "type_info": "Text" + }, + { + "ordinal": 3, + "name": "wg_ip", + "type_info": "Text" + }, + { + "ordinal": 4, + "name": "wg_endpoint", + "type_info": "Text" + }, + { + "ordinal": 5, + "name": "api_port", + "type_info": "Int4" + }, + { + "ordinal": 6, + "name": "status", + "type_info": "Text" + }, + { + "ordinal": 7, + "name": "version", + "type_info": "Text" + }, + { + "ordinal": 8, + "name": "last_heartbeat", + "type_info": "Timestamptz" + }, + { + "ordinal": 9, + "name": "created_at", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [ + "Uuid", + "Text", + "Text", + "Text", + "Text", + "Int4", + "Text", + "Bool" + ] + }, + "nullable": [ + false, + false, + false, + false, + true, + false, + false, + true, + true, + false + ] + }, + "hash": "5102fd11d51afd502b1490a7ab201325a108b99b20f00f325c5f578fe166d47a" +} diff --git a/lynx/.sqlx/query-53a2a9841fa11f694257194949b4ab849dacc55f62e7a64a0de22d97674d1fe4.json b/lynx/.sqlx/query-53a2a9841fa11f694257194949b4ab849dacc55f62e7a64a0de22d97674d1fe4.json new file mode 100644 index 0000000..8a0e1d6 --- /dev/null +++ b/lynx/.sqlx/query-53a2a9841fa11f694257194949b4ab849dacc55f62e7a64a0de22d97674d1fe4.json @@ -0,0 +1,12 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE migration_state SET status='notifying_agents', updated_at=NOW() WHERE id=1", + "describe": { + "columns": [], + "parameters": { + "Left": [] + }, + "nullable": [] + }, + "hash": "53a2a9841fa11f694257194949b4ab849dacc55f62e7a64a0de22d97674d1fe4" +} diff --git a/lynx/.sqlx/query-563588298c7a6d7d8c6ba0f81b2ca1757d55a7cc67577fd147527e9a0b036bb0.json b/lynx/.sqlx/query-563588298c7a6d7d8c6ba0f81b2ca1757d55a7cc67577fd147527e9a0b036bb0.json new file mode 100644 index 0000000..e156dfd --- /dev/null +++ b/lynx/.sqlx/query-563588298c7a6d7d8c6ba0f81b2ca1757d55a7cc67577fd147527e9a0b036bb0.json @@ -0,0 +1,15 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE domain_config\n SET cert_type=$1, cert_expires_at=$2, status='active', error_message=NULL, updated_at=NOW()\n WHERE id=1", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Text", + "Timestamptz" + ] + }, + "nullable": [] + }, + "hash": "563588298c7a6d7d8c6ba0f81b2ca1757d55a7cc67577fd147527e9a0b036bb0" +} diff --git a/lynx/.sqlx/query-573429710cfa33d134397c63dacbf3ab2fdb42cd3b5ad94337c294f61e520414.json b/lynx/.sqlx/query-573429710cfa33d134397c63dacbf3ab2fdb42cd3b5ad94337c294f61e520414.json new file mode 100644 index 0000000..183eb9e --- /dev/null +++ b/lynx/.sqlx/query-573429710cfa33d134397c63dacbf3ab2fdb42cd3b5ad94337c294f61e520414.json @@ -0,0 +1,15 @@ +{ + "db_name": "PostgreSQL", + "query": "\n INSERT INTO global_rule_sync (rule_id, agent_id)\n VALUES ($1, $2)\n ON CONFLICT (rule_id, agent_id) DO NOTHING\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid", + "Uuid" + ] + }, + "nullable": [] + }, + "hash": "573429710cfa33d134397c63dacbf3ab2fdb42cd3b5ad94337c294f61e520414" +} diff --git a/lynx/.sqlx/query-576a99e6b7c305f1396f0944664795d79be3e4d6e4606adf44e17a4d4144e3a4.json b/lynx/.sqlx/query-576a99e6b7c305f1396f0944664795d79be3e4d6e4606adf44e17a4d4144e3a4.json new file mode 100644 index 0000000..c143136 --- /dev/null +++ b/lynx/.sqlx/query-576a99e6b7c305f1396f0944664795d79be3e4d6e4606adf44e17a4d4144e3a4.json @@ -0,0 +1,16 @@ +{ + "db_name": "PostgreSQL", + "query": "\n INSERT INTO organization_members (organization_id, user_id, role)\n VALUES ($1, $2, $3)\n ON CONFLICT (organization_id, user_id) DO NOTHING\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid", + "Uuid", + "Text" + ] + }, + "nullable": [] + }, + "hash": "576a99e6b7c305f1396f0944664795d79be3e4d6e4606adf44e17a4d4144e3a4" +} diff --git a/lynx/.sqlx/query-586ba988eafbb6a0302acf07c795ba005b1061898fbbbf7adeadad8698928c7a.json b/lynx/.sqlx/query-586ba988eafbb6a0302acf07c795ba005b1061898fbbbf7adeadad8698928c7a.json new file mode 100644 index 0000000..aa07a66 --- /dev/null +++ b/lynx/.sqlx/query-586ba988eafbb6a0302acf07c795ba005b1061898fbbbf7adeadad8698928c7a.json @@ -0,0 +1,52 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT id, organization_id, agent_id, name, slug, created_at FROM projects WHERE organization_id = $1 ORDER BY created_at ASC", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "organization_id", + "type_info": "Uuid" + }, + { + "ordinal": 2, + "name": "agent_id", + "type_info": "Uuid" + }, + { + "ordinal": 3, + "name": "name", + "type_info": "Text" + }, + { + "ordinal": 4, + "name": "slug", + "type_info": "Text" + }, + { + "ordinal": 5, + "name": "created_at", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [ + false, + false, + false, + false, + false, + false + ] + }, + "hash": "586ba988eafbb6a0302acf07c795ba005b1061898fbbbf7adeadad8698928c7a" +} diff --git a/lynx/.sqlx/query-5c8172cc810f6fc92205f9b7f5b13995ddf99b29ad0ccae2a36f73638c9934b1.json b/lynx/.sqlx/query-5c8172cc810f6fc92205f9b7f5b13995ddf99b29ad0ccae2a36f73638c9934b1.json new file mode 100644 index 0000000..403ac53 --- /dev/null +++ b/lynx/.sqlx/query-5c8172cc810f6fc92205f9b7f5b13995ddf99b29ad0ccae2a36f73638c9934b1.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT EXISTS(\n SELECT 1 FROM role_permissions rp\n JOIN permissions p ON p.id = rp.permission_id\n WHERE rp.role_id = $1 AND p.key = '*:*'\n ) AS \"exists!\"", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "exists!", + "type_info": "Bool" + } + ], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [ + null + ] + }, + "hash": "5c8172cc810f6fc92205f9b7f5b13995ddf99b29ad0ccae2a36f73638c9934b1" +} diff --git a/lynx/.sqlx/query-5de6638fe1feb865f6b7c8d99f9d154aec690ddbd9d716d26c5026c73e93b22e.json b/lynx/.sqlx/query-5de6638fe1feb865f6b7c8d99f9d154aec690ddbd9d716d26c5026c73e93b22e.json new file mode 100644 index 0000000..8da54bb --- /dev/null +++ b/lynx/.sqlx/query-5de6638fe1feb865f6b7c8d99f9d154aec690ddbd9d716d26c5026c73e93b22e.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT EXISTS(SELECT 1 FROM users WHERE email_hash = $1)", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "exists", + "type_info": "Bool" + } + ], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [ + null + ] + }, + "hash": "5de6638fe1feb865f6b7c8d99f9d154aec690ddbd9d716d26c5026c73e93b22e" +} diff --git a/lynx/.sqlx/query-5e45ddba50fd7489ade01fc3aa1371282b126a7f5c8b8656bd59ac78531c429b.json b/lynx/.sqlx/query-5e45ddba50fd7489ade01fc3aa1371282b126a7f5c8b8656bd59ac78531c429b.json new file mode 100644 index 0000000..9bd33a8 --- /dev/null +++ b/lynx/.sqlx/query-5e45ddba50fd7489ade01fc3aa1371282b126a7f5c8b8656bd59ac78531c429b.json @@ -0,0 +1,16 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO roles (id, name, created_by) VALUES ($1, $2, $3)", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid", + "Text", + "Uuid" + ] + }, + "nullable": [] + }, + "hash": "5e45ddba50fd7489ade01fc3aa1371282b126a7f5c8b8656bd59ac78531c429b" +} diff --git a/lynx/.sqlx/query-617e651d593a9219f924ec16e6eba91a64a4d2eb269923541d0d7c92a87b2c06.json b/lynx/.sqlx/query-617e651d593a9219f924ec16e6eba91a64a4d2eb269923541d0d7c92a87b2c06.json new file mode 100644 index 0000000..01ca006 --- /dev/null +++ b/lynx/.sqlx/query-617e651d593a9219f924ec16e6eba91a64a4d2eb269923541d0d7c92a87b2c06.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT COALESCE(arch, 'x86_64') FROM agents WHERE id = $1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "coalesce", + "type_info": "Text" + } + ], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [ + null + ] + }, + "hash": "617e651d593a9219f924ec16e6eba91a64a4d2eb269923541d0d7c92a87b2c06" +} diff --git a/lynx/.sqlx/query-621e833479e86a16d3c95e9f4bd0b9576c3dc5cad3a61bf572c094535dfb248d.json b/lynx/.sqlx/query-621e833479e86a16d3c95e9f4bd0b9576c3dc5cad3a61bf572c094535dfb248d.json new file mode 100644 index 0000000..006fa8f --- /dev/null +++ b/lynx/.sqlx/query-621e833479e86a16d3c95e9f4bd0b9576c3dc5cad3a61bf572c094535dfb248d.json @@ -0,0 +1,16 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE agents SET status=$1, last_heartbeat=NOW(), version=$2 WHERE id=$3", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Text", + "Text", + "Uuid" + ] + }, + "nullable": [] + }, + "hash": "621e833479e86a16d3c95e9f4bd0b9576c3dc5cad3a61bf572c094535dfb248d" +} diff --git a/lynx/.sqlx/query-622a1ace62157c5a964a283b5bd82259c595eeef4e3a6acd2a6699fc448d4c8a.json b/lynx/.sqlx/query-622a1ace62157c5a964a283b5bd82259c595eeef4e3a6acd2a6699fc448d4c8a.json new file mode 100644 index 0000000..dd00180 --- /dev/null +++ b/lynx/.sqlx/query-622a1ace62157c5a964a283b5bd82259c595eeef4e3a6acd2a6699fc448d4c8a.json @@ -0,0 +1,20 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT migration_token_hash FROM migration_state WHERE id=1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "migration_token_hash", + "type_info": "Text" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + true + ] + }, + "hash": "622a1ace62157c5a964a283b5bd82259c595eeef4e3a6acd2a6699fc448d4c8a" +} diff --git a/lynx/.sqlx/query-639c7dd675a1e706426bed2d129eef64d0a88e5a7e3a0c3f5a34ff6c6ac689a7.json b/lynx/.sqlx/query-639c7dd675a1e706426bed2d129eef64d0a88e5a7e3a0c3f5a34ff6c6ac689a7.json new file mode 100644 index 0000000..e904d6a --- /dev/null +++ b/lynx/.sqlx/query-639c7dd675a1e706426bed2d129eef64d0a88e5a7e3a0c3f5a34ff6c6ac689a7.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT sync_token_hash FROM agents WHERE id = $1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "sync_token_hash", + "type_info": "Text" + } + ], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [ + true + ] + }, + "hash": "639c7dd675a1e706426bed2d129eef64d0a88e5a7e3a0c3f5a34ff6c6ac689a7" +} diff --git a/lynx/.sqlx/query-64b8c83f2abaf52c976abf08bae528199de49cb5ef2b84a035b16059b73a315e.json b/lynx/.sqlx/query-64b8c83f2abaf52c976abf08bae528199de49cb5ef2b84a035b16059b73a315e.json new file mode 100644 index 0000000..adab702 --- /dev/null +++ b/lynx/.sqlx/query-64b8c83f2abaf52c976abf08bae528199de49cb5ef2b84a035b16059b73a315e.json @@ -0,0 +1,12 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE domain_config SET status='active', cert_type='lets_encrypt', cert_expires_at=NOW() + INTERVAL '90 days', updated_at=NOW() WHERE id=1", + "describe": { + "columns": [], + "parameters": { + "Left": [] + }, + "nullable": [] + }, + "hash": "64b8c83f2abaf52c976abf08bae528199de49cb5ef2b84a035b16059b73a315e" +} diff --git a/lynx/.sqlx/query-64b9345af9f35211fd28e6ca77ffa6eff7e9153696b52f9e4e930bd012df3a5f.json b/lynx/.sqlx/query-64b9345af9f35211fd28e6ca77ffa6eff7e9153696b52f9e4e930bd012df3a5f.json new file mode 100644 index 0000000..5539875 --- /dev/null +++ b/lynx/.sqlx/query-64b9345af9f35211fd28e6ca77ffa6eff7e9153696b52f9e4e930bd012df3a5f.json @@ -0,0 +1,12 @@ +{ + "db_name": "PostgreSQL", + "query": "DELETE FROM sessions WHERE expires_at > NOW()", + "describe": { + "columns": [], + "parameters": { + "Left": [] + }, + "nullable": [] + }, + "hash": "64b9345af9f35211fd28e6ca77ffa6eff7e9153696b52f9e4e930bd012df3a5f" +} diff --git a/lynx/.sqlx/query-65f5f75262df10e25cbf75173662cde46a4e0ba48bc341ef0ab1938506850d9d.json b/lynx/.sqlx/query-65f5f75262df10e25cbf75173662cde46a4e0ba48bc341ef0ab1938506850d9d.json new file mode 100644 index 0000000..7999939 --- /dev/null +++ b/lynx/.sqlx/query-65f5f75262df10e25cbf75173662cde46a4e0ba48bc341ef0ab1938506850d9d.json @@ -0,0 +1,15 @@ +{ + "db_name": "PostgreSQL", + "query": "DELETE FROM user_roles WHERE user_id = $1 AND role_id = $2", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid", + "Uuid" + ] + }, + "nullable": [] + }, + "hash": "65f5f75262df10e25cbf75173662cde46a4e0ba48bc341ef0ab1938506850d9d" +} diff --git a/lynx/.sqlx/query-6627a3626d64ddf45ea6057054f6964dd9c2f7b874f8dcc8f81a4bca699b294c.json b/lynx/.sqlx/query-6627a3626d64ddf45ea6057054f6964dd9c2f7b874f8dcc8f81a4bca699b294c.json new file mode 100644 index 0000000..5ac404e --- /dev/null +++ b/lynx/.sqlx/query-6627a3626d64ddf45ea6057054f6964dd9c2f7b874f8dcc8f81a4bca699b294c.json @@ -0,0 +1,40 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT id, user_id, refresh_token_hash, expires_at\n FROM sessions\n WHERE refresh_token_hash = $1\n AND expires_at > NOW()\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "user_id", + "type_info": "Uuid" + }, + { + "ordinal": 2, + "name": "refresh_token_hash", + "type_info": "Text" + }, + { + "ordinal": 3, + "name": "expires_at", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [ + false, + false, + false, + false + ] + }, + "hash": "6627a3626d64ddf45ea6057054f6964dd9c2f7b874f8dcc8f81a4bca699b294c" +} diff --git a/lynx/.sqlx/query-663d3a4b8d35369a26a87babe28e095cd58598ec1fe4bd737b6d97420ff0595b.json b/lynx/.sqlx/query-663d3a4b8d35369a26a87babe28e095cd58598ec1fe4bd737b6d97420ff0595b.json new file mode 100644 index 0000000..375207c --- /dev/null +++ b/lynx/.sqlx/query-663d3a4b8d35369a26a87babe28e095cd58598ec1fe4bd737b6d97420ff0595b.json @@ -0,0 +1,15 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE agents SET status=$1, last_heartbeat=NOW() WHERE id=$2", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Text", + "Uuid" + ] + }, + "nullable": [] + }, + "hash": "663d3a4b8d35369a26a87babe28e095cd58598ec1fe4bd737b6d97420ff0595b" +} diff --git a/lynx/.sqlx/query-68afa06b76ee08c71260c00d81b094726dd1ad7f0c7b4640ea511b31b0ceabd7.json b/lynx/.sqlx/query-68afa06b76ee08c71260c00d81b094726dd1ad7f0c7b4640ea511b31b0ceabd7.json new file mode 100644 index 0000000..c324151 --- /dev/null +++ b/lynx/.sqlx/query-68afa06b76ee08c71260c00d81b094726dd1ad7f0c7b4640ea511b31b0ceabd7.json @@ -0,0 +1,20 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT EXISTS(\n SELECT 1 FROM user_roles ur\n JOIN role_permissions rp ON rp.role_id = ur.role_id\n JOIN permissions p ON p.id = rp.permission_id\n WHERE p.key = '*:*'\n )\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "exists", + "type_info": "Bool" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + null + ] + }, + "hash": "68afa06b76ee08c71260c00d81b094726dd1ad7f0c7b4640ea511b31b0ceabd7" +} diff --git a/lynx/.sqlx/query-69b6c00fd7ce74da9ed65c826367c3c12ba071ca56465864d84472d8b62a1a5c.json b/lynx/.sqlx/query-69b6c00fd7ce74da9ed65c826367c3c12ba071ca56465864d84472d8b62a1a5c.json new file mode 100644 index 0000000..df4c83d --- /dev/null +++ b/lynx/.sqlx/query-69b6c00fd7ce74da9ed65c826367c3c12ba071ca56465864d84472d8b62a1a5c.json @@ -0,0 +1,15 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE users SET single_session = $1 WHERE id = $2", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Bool", + "Uuid" + ] + }, + "nullable": [] + }, + "hash": "69b6c00fd7ce74da9ed65c826367c3c12ba071ca56465864d84472d8b62a1a5c" +} diff --git a/lynx/.sqlx/query-6a69dac17737d8209d8ddf9fef4142a63355320e75a8379e29b6879b8ba93a73.json b/lynx/.sqlx/query-6a69dac17737d8209d8ddf9fef4142a63355320e75a8379e29b6879b8ba93a73.json new file mode 100644 index 0000000..39c026c --- /dev/null +++ b/lynx/.sqlx/query-6a69dac17737d8209d8ddf9fef4142a63355320e75a8379e29b6879b8ba93a73.json @@ -0,0 +1,12 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE migration_state SET agents_confirmed = agents_confirmed + 1, updated_at=NOW() WHERE id=1", + "describe": { + "columns": [], + "parameters": { + "Left": [] + }, + "nullable": [] + }, + "hash": "6a69dac17737d8209d8ddf9fef4142a63355320e75a8379e29b6879b8ba93a73" +} diff --git a/lynx/.sqlx/query-6b359c6243c482867b6fc4f7e2e3086f797a6452b27c7c9ce2eef6f1d4932fbd.json b/lynx/.sqlx/query-6b359c6243c482867b6fc4f7e2e3086f797a6452b27c7c9ce2eef6f1d4932fbd.json new file mode 100644 index 0000000..b605b31 --- /dev/null +++ b/lynx/.sqlx/query-6b359c6243c482867b6fc4f7e2e3086f797a6452b27c7c9ce2eef6f1d4932fbd.json @@ -0,0 +1,20 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT status FROM domain_config WHERE id = 1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "status", + "type_info": "Text" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + false + ] + }, + "hash": "6b359c6243c482867b6fc4f7e2e3086f797a6452b27c7c9ce2eef6f1d4932fbd" +} diff --git a/lynx/.sqlx/query-6b96b6b09dd833489fd9042ea7b486e8da49b3263e2051b9c5df7f9d134e25dd.json b/lynx/.sqlx/query-6b96b6b09dd833489fd9042ea7b486e8da49b3263e2051b9c5df7f9d134e25dd.json new file mode 100644 index 0000000..2b0c526 --- /dev/null +++ b/lynx/.sqlx/query-6b96b6b09dd833489fd9042ea7b486e8da49b3263e2051b9c5df7f9d134e25dd.json @@ -0,0 +1,15 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE ip_pool SET agent_id = $1, updated_at = NOW() WHERE ip::text = $2", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid", + "Text" + ] + }, + "nullable": [] + }, + "hash": "6b96b6b09dd833489fd9042ea7b486e8da49b3263e2051b9c5df7f9d134e25dd" +} diff --git a/lynx/.sqlx/query-6d5dc21c6a9e2864fca14e3babe32f70a23626f102a4055a47d154f0e8c8e52a.json b/lynx/.sqlx/query-6d5dc21c6a9e2864fca14e3babe32f70a23626f102a4055a47d154f0e8c8e52a.json new file mode 100644 index 0000000..cb8d5dd --- /dev/null +++ b/lynx/.sqlx/query-6d5dc21c6a9e2864fca14e3babe32f70a23626f102a4055a47d154f0e8c8e52a.json @@ -0,0 +1,12 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE users SET force_password_change = TRUE", + "describe": { + "columns": [], + "parameters": { + "Left": [] + }, + "nullable": [] + }, + "hash": "6d5dc21c6a9e2864fca14e3babe32f70a23626f102a4055a47d154f0e8c8e52a" +} diff --git a/lynx/.sqlx/query-70cc31c3bd3d6543324220b870f7705b3e6b4886fc4ef18b0b020d90cfdc5fb3.json b/lynx/.sqlx/query-70cc31c3bd3d6543324220b870f7705b3e6b4886fc4ef18b0b020d90cfdc5fb3.json new file mode 100644 index 0000000..e01a32b --- /dev/null +++ b/lynx/.sqlx/query-70cc31c3bd3d6543324220b870f7705b3e6b4886fc4ef18b0b020d90cfdc5fb3.json @@ -0,0 +1,40 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT m.user_id, u.username, m.role, m.joined_at\n FROM organization_members m\n JOIN users u ON u.id = m.user_id\n WHERE m.organization_id = $1\n ORDER BY m.joined_at ASC\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "user_id", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "username", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "role", + "type_info": "Text" + }, + { + "ordinal": 3, + "name": "joined_at", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [ + false, + false, + false, + false + ] + }, + "hash": "70cc31c3bd3d6543324220b870f7705b3e6b4886fc4ef18b0b020d90cfdc5fb3" +} diff --git a/lynx/.sqlx/query-70de9a54908258f9ffff93ba42efdb5407b9a9ed9425098d0c2544b6541a4d7e.json b/lynx/.sqlx/query-70de9a54908258f9ffff93ba42efdb5407b9a9ed9425098d0c2544b6541a4d7e.json new file mode 100644 index 0000000..243f882 --- /dev/null +++ b/lynx/.sqlx/query-70de9a54908258f9ffff93ba42efdb5407b9a9ed9425098d0c2544b6541a4d7e.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT COUNT(DISTINCT ur.user_id) FROM user_roles ur\n JOIN role_permissions rp ON rp.role_id = ur.role_id\n JOIN permissions p ON p.id = rp.permission_id\n WHERE ur.role_id = $1 AND p.key = '*:*'\n AND NOT EXISTS (\n SELECT 1 FROM user_roles ur2\n JOIN role_permissions rp2 ON rp2.role_id = ur2.role_id\n JOIN permissions p2 ON p2.id = rp2.permission_id\n WHERE ur2.user_id = ur.user_id AND ur2.role_id != $1 AND p2.key = '*:*'\n )", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "count", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [ + null + ] + }, + "hash": "70de9a54908258f9ffff93ba42efdb5407b9a9ed9425098d0c2544b6541a4d7e" +} diff --git a/lynx/.sqlx/query-712fdd722044311a97343c524de4bfbf8375cee56d14643670072b670b347195.json b/lynx/.sqlx/query-712fdd722044311a97343c524de4bfbf8375cee56d14643670072b670b347195.json new file mode 100644 index 0000000..8046594 --- /dev/null +++ b/lynx/.sqlx/query-712fdd722044311a97343c524de4bfbf8375cee56d14643670072b670b347195.json @@ -0,0 +1,17 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO security_alerts (id, kind, detail, agent_id) VALUES ($1, $2, $3, $4)", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid", + "Text", + "Text", + "Uuid" + ] + }, + "nullable": [] + }, + "hash": "712fdd722044311a97343c524de4bfbf8375cee56d14643670072b670b347195" +} diff --git a/lynx/.sqlx/query-73ac44f1c18b35583637cc51a439f147feacfcbe765c69a63d20621cce25fb8a.json b/lynx/.sqlx/query-73ac44f1c18b35583637cc51a439f147feacfcbe765c69a63d20621cce25fb8a.json new file mode 100644 index 0000000..20e9a07 --- /dev/null +++ b/lynx/.sqlx/query-73ac44f1c18b35583637cc51a439f147feacfcbe765c69a63d20621cce25fb8a.json @@ -0,0 +1,40 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT id, password_hash, force_password_change, single_session FROM users WHERE username = $1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "password_hash", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "force_password_change", + "type_info": "Bool" + }, + { + "ordinal": 3, + "name": "single_session", + "type_info": "Bool" + } + ], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [ + false, + false, + false, + false + ] + }, + "hash": "73ac44f1c18b35583637cc51a439f147feacfcbe765c69a63d20621cce25fb8a" +} diff --git a/lynx/.sqlx/query-76269ec9536854980b4274646932c8dc6eb760fbcbe636596dbaa7e8963dbed3.json b/lynx/.sqlx/query-76269ec9536854980b4274646932c8dc6eb760fbcbe636596dbaa7e8963dbed3.json new file mode 100644 index 0000000..d8b7773 --- /dev/null +++ b/lynx/.sqlx/query-76269ec9536854980b4274646932c8dc6eb760fbcbe636596dbaa7e8963dbed3.json @@ -0,0 +1,15 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO agent_events (id, agent_id, event, detail) VALUES ($1, $2, 'heartbeat_lost', NULL)", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid", + "Uuid" + ] + }, + "nullable": [] + }, + "hash": "76269ec9536854980b4274646932c8dc6eb760fbcbe636596dbaa7e8963dbed3" +} diff --git a/lynx/.sqlx/query-76a7e92c144ac7ff3992987838d894bd58d2bf0e4f61101192fece85284d40ff.json b/lynx/.sqlx/query-76a7e92c144ac7ff3992987838d894bd58d2bf0e4f61101192fece85284d40ff.json new file mode 100644 index 0000000..fa89f1d --- /dev/null +++ b/lynx/.sqlx/query-76a7e92c144ac7ff3992987838d894bd58d2bf0e4f61101192fece85284d40ff.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT EXISTS(SELECT 1 FROM users WHERE id = $1) AS \"exists!\"", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "exists!", + "type_info": "Bool" + } + ], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [ + null + ] + }, + "hash": "76a7e92c144ac7ff3992987838d894bd58d2bf0e4f61101192fece85284d40ff" +} diff --git a/lynx/.sqlx/query-772e7f05553ab4e646c67af8984fa9e624ef440f2c552997e859c3093d973961.json b/lynx/.sqlx/query-772e7f05553ab4e646c67af8984fa9e624ef440f2c552997e859c3093d973961.json new file mode 100644 index 0000000..acedea9 --- /dev/null +++ b/lynx/.sqlx/query-772e7f05553ab4e646c67af8984fa9e624ef440f2c552997e859c3093d973961.json @@ -0,0 +1,53 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT id, organization_id, agent_id, name, slug, created_at FROM projects WHERE id = $1 AND organization_id = $2", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "organization_id", + "type_info": "Uuid" + }, + { + "ordinal": 2, + "name": "agent_id", + "type_info": "Uuid" + }, + { + "ordinal": 3, + "name": "name", + "type_info": "Text" + }, + { + "ordinal": 4, + "name": "slug", + "type_info": "Text" + }, + { + "ordinal": 5, + "name": "created_at", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [ + "Uuid", + "Uuid" + ] + }, + "nullable": [ + false, + false, + false, + false, + false, + false + ] + }, + "hash": "772e7f05553ab4e646c67af8984fa9e624ef440f2c552997e859c3093d973961" +} diff --git a/lynx/.sqlx/query-79d72babe1eda061141c45a0b860ab42825a304ef823555fd61db303c8a852ba.json b/lynx/.sqlx/query-79d72babe1eda061141c45a0b860ab42825a304ef823555fd61db303c8a852ba.json new file mode 100644 index 0000000..e28a56b --- /dev/null +++ b/lynx/.sqlx/query-79d72babe1eda061141c45a0b860ab42825a304ef823555fd61db303c8a852ba.json @@ -0,0 +1,23 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT 1 FROM projects WHERE id = $1 AND organization_id = $2", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "?column?", + "type_info": "Int4" + } + ], + "parameters": { + "Left": [ + "Uuid", + "Uuid" + ] + }, + "nullable": [ + null + ] + }, + "hash": "79d72babe1eda061141c45a0b860ab42825a304ef823555fd61db303c8a852ba" +} diff --git a/lynx/.sqlx/query-7a2c43464e88166ffe4e4103dbf1867d853659b5b4fd6d352583a254d0a7a18e.json b/lynx/.sqlx/query-7a2c43464e88166ffe4e4103dbf1867d853659b5b4fd6d352583a254d0a7a18e.json new file mode 100644 index 0000000..88f31d7 --- /dev/null +++ b/lynx/.sqlx/query-7a2c43464e88166ffe4e4103dbf1867d853659b5b4fd6d352583a254d0a7a18e.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT entry_hash FROM audit_log WHERE agent_id = $1 ORDER BY created_at DESC LIMIT 1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "entry_hash", + "type_info": "Text" + } + ], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [ + false + ] + }, + "hash": "7a2c43464e88166ffe4e4103dbf1867d853659b5b4fd6d352583a254d0a7a18e" +} diff --git a/lynx/.sqlx/query-7aa9e8d60074416941adc937e4b63890bab9a496d1ae69a3fbb6500910e136f8.json b/lynx/.sqlx/query-7aa9e8d60074416941adc937e4b63890bab9a496d1ae69a3fbb6500910e136f8.json new file mode 100644 index 0000000..20f1af0 --- /dev/null +++ b/lynx/.sqlx/query-7aa9e8d60074416941adc937e4b63890bab9a496d1ae69a3fbb6500910e136f8.json @@ -0,0 +1,12 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE migration_state SET status='transferring', updated_at=NOW() WHERE id=1", + "describe": { + "columns": [], + "parameters": { + "Left": [] + }, + "nullable": [] + }, + "hash": "7aa9e8d60074416941adc937e4b63890bab9a496d1ae69a3fbb6500910e136f8" +} diff --git a/lynx/.sqlx/query-7bf3f04bf7a71c6c23c14d30ca81ff89b849ac4235df869801f34f19901b5445.json b/lynx/.sqlx/query-7bf3f04bf7a71c6c23c14d30ca81ff89b849ac4235df869801f34f19901b5445.json new file mode 100644 index 0000000..a7b00ce --- /dev/null +++ b/lynx/.sqlx/query-7bf3f04bf7a71c6c23c14d30ca81ff89b849ac4235df869801f34f19901b5445.json @@ -0,0 +1,74 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT id, status, role, target_url, agents_total, agents_confirmed,\n error_message, started_at, completed_at, updated_at\n FROM migration_state WHERE id = 1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int4" + }, + { + "ordinal": 1, + "name": "status", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "role", + "type_info": "Text" + }, + { + "ordinal": 3, + "name": "target_url", + "type_info": "Text" + }, + { + "ordinal": 4, + "name": "agents_total", + "type_info": "Int4" + }, + { + "ordinal": 5, + "name": "agents_confirmed", + "type_info": "Int4" + }, + { + "ordinal": 6, + "name": "error_message", + "type_info": "Text" + }, + { + "ordinal": 7, + "name": "started_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 8, + "name": "completed_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 9, + "name": "updated_at", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + false, + false, + false, + true, + false, + false, + true, + true, + true, + false + ] + }, + "hash": "7bf3f04bf7a71c6c23c14d30ca81ff89b849ac4235df869801f34f19901b5445" +} diff --git a/lynx/.sqlx/query-825215844cc17aede2cff3845f2ac2bfe0a301005821fb018c60f3dda7c5f0e0.json b/lynx/.sqlx/query-825215844cc17aede2cff3845f2ac2bfe0a301005821fb018c60f3dda7c5f0e0.json new file mode 100644 index 0000000..14d4ec6 --- /dev/null +++ b/lynx/.sqlx/query-825215844cc17aede2cff3845f2ac2bfe0a301005821fb018c60f3dda7c5f0e0.json @@ -0,0 +1,28 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT id, last_jti FROM sessions WHERE user_id = $1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "last_jti", + "type_info": "Uuid" + } + ], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [ + false, + true + ] + }, + "hash": "825215844cc17aede2cff3845f2ac2bfe0a301005821fb018c60f3dda7c5f0e0" +} diff --git a/lynx/.sqlx/query-897e779d71e87a660237ebe5c1bd4bd8861eca3c4451618be43b579f134f557d.json b/lynx/.sqlx/query-897e779d71e87a660237ebe5c1bd4bd8861eca3c4451618be43b579f134f557d.json new file mode 100644 index 0000000..99bfde8 --- /dev/null +++ b/lynx/.sqlx/query-897e779d71e87a660237ebe5c1bd4bd8861eca3c4451618be43b579f134f557d.json @@ -0,0 +1,23 @@ +{ + "db_name": "PostgreSQL", + "query": "DELETE FROM sessions WHERE id = $1 AND user_id = $2 RETURNING last_jti", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "last_jti", + "type_info": "Uuid" + } + ], + "parameters": { + "Left": [ + "Uuid", + "Uuid" + ] + }, + "nullable": [ + true + ] + }, + "hash": "897e779d71e87a660237ebe5c1bd4bd8861eca3c4451618be43b579f134f557d" +} diff --git a/lynx/.sqlx/query-8bd96aa5c97181d1dbe44ea92d19eedaea4bdbdbc8a974b289992eb6885f9007.json b/lynx/.sqlx/query-8bd96aa5c97181d1dbe44ea92d19eedaea4bdbdbc8a974b289992eb6885f9007.json new file mode 100644 index 0000000..d789999 --- /dev/null +++ b/lynx/.sqlx/query-8bd96aa5c97181d1dbe44ea92d19eedaea4bdbdbc8a974b289992eb6885f9007.json @@ -0,0 +1,17 @@ +{ + "db_name": "PostgreSQL", + "query": "\n INSERT INTO rotation_log (id, triggered_by, reason, scope)\n VALUES ($1, $2, $3, $4)\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid", + "Uuid", + "Text", + "Text" + ] + }, + "nullable": [] + }, + "hash": "8bd96aa5c97181d1dbe44ea92d19eedaea4bdbdbc8a974b289992eb6885f9007" +} diff --git a/lynx/.sqlx/query-8fcd12f6016d9fd33fd10bb3337858feaaf63c7f83fa8a3a8abd2b3e57f35535.json b/lynx/.sqlx/query-8fcd12f6016d9fd33fd10bb3337858feaaf63c7f83fa8a3a8abd2b3e57f35535.json new file mode 100644 index 0000000..010ae64 --- /dev/null +++ b/lynx/.sqlx/query-8fcd12f6016d9fd33fd10bb3337858feaaf63c7f83fa8a3a8abd2b3e57f35535.json @@ -0,0 +1,17 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO role_permissions (id, role_id, permission_id, created_by) VALUES ($1, $2, $3, $4)\n ON CONFLICT (role_id, permission_id) DO NOTHING", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid", + "Uuid", + "Uuid", + "Uuid" + ] + }, + "nullable": [] + }, + "hash": "8fcd12f6016d9fd33fd10bb3337858feaaf63c7f83fa8a3a8abd2b3e57f35535" +} diff --git a/lynx/.sqlx/query-91aa2e4739161f9f8786c84877b18aabeef08fedd2c9f78586e654e35d7ea778.json b/lynx/.sqlx/query-91aa2e4739161f9f8786c84877b18aabeef08fedd2c9f78586e654e35d7ea778.json new file mode 100644 index 0000000..4170031 --- /dev/null +++ b/lynx/.sqlx/query-91aa2e4739161f9f8786c84877b18aabeef08fedd2c9f78586e654e35d7ea778.json @@ -0,0 +1,50 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT company_name, logo_url, primary_color, secondary_color, accent_color, updated_at FROM white_label WHERE id = 1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "company_name", + "type_info": "Text" + }, + { + "ordinal": 1, + "name": "logo_url", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "primary_color", + "type_info": "Text" + }, + { + "ordinal": 3, + "name": "secondary_color", + "type_info": "Text" + }, + { + "ordinal": 4, + "name": "accent_color", + "type_info": "Text" + }, + { + "ordinal": 5, + "name": "updated_at", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + false, + true, + false, + false, + false, + false + ] + }, + "hash": "91aa2e4739161f9f8786c84877b18aabeef08fedd2c9f78586e654e35d7ea778" +} diff --git a/lynx/.sqlx/query-924b5f89e41e330bb578e2c4cdac1157ae4b785fe9fd5313a842188e33f9be31.json b/lynx/.sqlx/query-924b5f89e41e330bb578e2c4cdac1157ae4b785fe9fd5313a842188e33f9be31.json new file mode 100644 index 0000000..2da341b --- /dev/null +++ b/lynx/.sqlx/query-924b5f89e41e330bb578e2c4cdac1157ae4b785fe9fd5313a842188e33f9be31.json @@ -0,0 +1,68 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT id, triggered_by, version, channel, scope, agent_id, status, error, created_at\n FROM update_log\n ORDER BY created_at DESC\n LIMIT 50\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "triggered_by", + "type_info": "Uuid" + }, + { + "ordinal": 2, + "name": "version", + "type_info": "Text" + }, + { + "ordinal": 3, + "name": "channel", + "type_info": "Text" + }, + { + "ordinal": 4, + "name": "scope", + "type_info": "Text" + }, + { + "ordinal": 5, + "name": "agent_id", + "type_info": "Uuid" + }, + { + "ordinal": 6, + "name": "status", + "type_info": "Text" + }, + { + "ordinal": 7, + "name": "error", + "type_info": "Text" + }, + { + "ordinal": 8, + "name": "created_at", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + false, + true, + false, + false, + false, + true, + false, + true, + false + ] + }, + "hash": "924b5f89e41e330bb578e2c4cdac1157ae4b785fe9fd5313a842188e33f9be31" +} diff --git a/lynx/.sqlx/query-9278b37726d138938ca9c9ea2b55e270aede83768d10e93033a360c877493589.json b/lynx/.sqlx/query-9278b37726d138938ca9c9ea2b55e270aede83768d10e93033a360c877493589.json new file mode 100644 index 0000000..376c29a --- /dev/null +++ b/lynx/.sqlx/query-9278b37726d138938ca9c9ea2b55e270aede83768d10e93033a360c877493589.json @@ -0,0 +1,26 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT agents_total, agents_confirmed FROM migration_state WHERE id=1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "agents_total", + "type_info": "Int4" + }, + { + "ordinal": 1, + "name": "agents_confirmed", + "type_info": "Int4" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + false, + false + ] + }, + "hash": "9278b37726d138938ca9c9ea2b55e270aede83768d10e93033a360c877493589" +} diff --git a/lynx/.sqlx/query-92aa51b8f292d7a7ad739b53adba78adff789460ab2fef1a7a47731cebd37a2b.json b/lynx/.sqlx/query-92aa51b8f292d7a7ad739b53adba78adff789460ab2fef1a7a47731cebd37a2b.json new file mode 100644 index 0000000..ee69b69 --- /dev/null +++ b/lynx/.sqlx/query-92aa51b8f292d7a7ad739b53adba78adff789460ab2fef1a7a47731cebd37a2b.json @@ -0,0 +1,20 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT MAX(created_at) FROM rotation_log WHERE reason IN ('scheduled', 'update')", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "max", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + null + ] + }, + "hash": "92aa51b8f292d7a7ad739b53adba78adff789460ab2fef1a7a47731cebd37a2b" +} diff --git a/lynx/.sqlx/query-949496a62ce0e92243c088e183afa5ce958a40c62f069ae37d7348bdb607a9d3.json b/lynx/.sqlx/query-949496a62ce0e92243c088e183afa5ce958a40c62f069ae37d7348bdb607a9d3.json new file mode 100644 index 0000000..66493c6 --- /dev/null +++ b/lynx/.sqlx/query-949496a62ce0e92243c088e183afa5ce958a40c62f069ae37d7348bdb607a9d3.json @@ -0,0 +1,14 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE ip_pool SET agent_id = NULL, updated_at = NOW() WHERE agent_id = $1", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [] + }, + "hash": "949496a62ce0e92243c088e183afa5ce958a40c62f069ae37d7348bdb607a9d3" +} diff --git a/lynx/.sqlx/query-94b1cb3a0ba7f37ccfc86c83499caa7c5ba238d97731f21cb4f366d676d039e4.json b/lynx/.sqlx/query-94b1cb3a0ba7f37ccfc86c83499caa7c5ba238d97731f21cb4f366d676d039e4.json new file mode 100644 index 0000000..d83d217 --- /dev/null +++ b/lynx/.sqlx/query-94b1cb3a0ba7f37ccfc86c83499caa7c5ba238d97731f21cb4f366d676d039e4.json @@ -0,0 +1,28 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT wg_ip::text AS wg_ip, api_port FROM agents WHERE id = $1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "wg_ip", + "type_info": "Text" + }, + { + "ordinal": 1, + "name": "api_port", + "type_info": "Int4" + } + ], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [ + false, + false + ] + }, + "hash": "94b1cb3a0ba7f37ccfc86c83499caa7c5ba238d97731f21cb4f366d676d039e4" +} diff --git a/lynx/.sqlx/query-95c362b52fb98487642ea5875c52eb5d584b9414631dad761007efc610fb16cd.json b/lynx/.sqlx/query-95c362b52fb98487642ea5875c52eb5d584b9414631dad761007efc610fb16cd.json new file mode 100644 index 0000000..76e018c --- /dev/null +++ b/lynx/.sqlx/query-95c362b52fb98487642ea5875c52eb5d584b9414631dad761007efc610fb16cd.json @@ -0,0 +1,20 @@ +{ + "db_name": "PostgreSQL", + "query": "\n INSERT INTO sessions (id, user_id, ip, user_agent, refresh_token_hash, expires_at, last_jti)\n VALUES ($1, $2, $3, $4, $5, $6, $7)\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid", + "Uuid", + "Text", + "Text", + "Text", + "Timestamptz", + "Uuid" + ] + }, + "nullable": [] + }, + "hash": "95c362b52fb98487642ea5875c52eb5d584b9414631dad761007efc610fb16cd" +} diff --git a/lynx/.sqlx/query-976b52de19f415be5fbbf5b025df2668dbc489dcf6f20442d4f8ef635977c6d6.json b/lynx/.sqlx/query-976b52de19f415be5fbbf5b025df2668dbc489dcf6f20442d4f8ef635977c6d6.json new file mode 100644 index 0000000..8fe79dc --- /dev/null +++ b/lynx/.sqlx/query-976b52de19f415be5fbbf5b025df2668dbc489dcf6f20442d4f8ef635977c6d6.json @@ -0,0 +1,20 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT COUNT(*) FROM agents", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "count", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + null + ] + }, + "hash": "976b52de19f415be5fbbf5b025df2668dbc489dcf6f20442d4f8ef635977c6d6" +} diff --git a/lynx/.sqlx/query-977b4f94a6a1951a48c3526187a598d18f2c3da9c280c151a9786cd3ae0c18b6.json b/lynx/.sqlx/query-977b4f94a6a1951a48c3526187a598d18f2c3da9c280c151a9786cd3ae0c18b6.json new file mode 100644 index 0000000..316f6aa --- /dev/null +++ b/lynx/.sqlx/query-977b4f94a6a1951a48c3526187a598d18f2c3da9c280c151a9786cd3ae0c18b6.json @@ -0,0 +1,35 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT agent_a_id, agent_b_id, replica_count FROM data_plane_tunnels WHERE id=$1 AND project_id=$2", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "agent_a_id", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "agent_b_id", + "type_info": "Uuid" + }, + { + "ordinal": 2, + "name": "replica_count", + "type_info": "Int4" + } + ], + "parameters": { + "Left": [ + "Uuid", + "Uuid" + ] + }, + "nullable": [ + false, + false, + false + ] + }, + "hash": "977b4f94a6a1951a48c3526187a598d18f2c3da9c280c151a9786cd3ae0c18b6" +} diff --git a/lynx/.sqlx/query-9a5b430585bf8c19c0a5915dfd5744d4b33edd500e7027cd051302debc29fa53.json b/lynx/.sqlx/query-9a5b430585bf8c19c0a5915dfd5744d4b33edd500e7027cd051302debc29fa53.json new file mode 100644 index 0000000..4215ae9 --- /dev/null +++ b/lynx/.sqlx/query-9a5b430585bf8c19c0a5915dfd5744d4b33edd500e7027cd051302debc29fa53.json @@ -0,0 +1,14 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE global_rule_sync SET synced_at = NOW() WHERE agent_id = $1 AND synced_at IS NULL", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [] + }, + "hash": "9a5b430585bf8c19c0a5915dfd5744d4b33edd500e7027cd051302debc29fa53" +} diff --git a/lynx/.sqlx/query-9ac83fc6f92c179ef552bd00c922cf22eb1665a8248f931d18dcd4dcb9759cb9.json b/lynx/.sqlx/query-9ac83fc6f92c179ef552bd00c922cf22eb1665a8248f931d18dcd4dcb9759cb9.json new file mode 100644 index 0000000..0faa151 --- /dev/null +++ b/lynx/.sqlx/query-9ac83fc6f92c179ef552bd00c922cf22eb1665a8248f931d18dcd4dcb9759cb9.json @@ -0,0 +1,116 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT id, scope, agent_id, kind, port, port_end, protocol,\n ip_list, ip_version, rate_per_min, description,\n priority, enabled, direction, created_by, created_at, updated_at\n FROM nftables_rules\n WHERE scope = 'global'\n ORDER BY priority ASC, created_at ASC\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "scope", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "agent_id", + "type_info": "Uuid" + }, + { + "ordinal": 3, + "name": "kind", + "type_info": "Text" + }, + { + "ordinal": 4, + "name": "port", + "type_info": "Int4" + }, + { + "ordinal": 5, + "name": "port_end", + "type_info": "Int4" + }, + { + "ordinal": 6, + "name": "protocol", + "type_info": "Text" + }, + { + "ordinal": 7, + "name": "ip_list", + "type_info": "TextArray" + }, + { + "ordinal": 8, + "name": "ip_version", + "type_info": "Text" + }, + { + "ordinal": 9, + "name": "rate_per_min", + "type_info": "Int4" + }, + { + "ordinal": 10, + "name": "description", + "type_info": "Text" + }, + { + "ordinal": 11, + "name": "priority", + "type_info": "Int4" + }, + { + "ordinal": 12, + "name": "enabled", + "type_info": "Bool" + }, + { + "ordinal": 13, + "name": "direction", + "type_info": "Text" + }, + { + "ordinal": 14, + "name": "created_by", + "type_info": "Uuid" + }, + { + "ordinal": 15, + "name": "created_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 16, + "name": "updated_at", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + false, + false, + true, + false, + true, + true, + true, + false, + false, + true, + true, + false, + false, + false, + true, + false, + false + ] + }, + "hash": "9ac83fc6f92c179ef552bd00c922cf22eb1665a8248f931d18dcd4dcb9759cb9" +} diff --git a/lynx/.sqlx/query-9b2052904dc1f773809600a88eb61bb8164b8013f7e2c4b6c2db15077e554cb1.json b/lynx/.sqlx/query-9b2052904dc1f773809600a88eb61bb8164b8013f7e2c4b6c2db15077e554cb1.json new file mode 100644 index 0000000..d384b48 --- /dev/null +++ b/lynx/.sqlx/query-9b2052904dc1f773809600a88eb61bb8164b8013f7e2c4b6c2db15077e554cb1.json @@ -0,0 +1,32 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT status, domain, cert_type FROM domain_config WHERE id = 1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "status", + "type_info": "Text" + }, + { + "ordinal": 1, + "name": "domain", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "cert_type", + "type_info": "Text" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + false, + true, + false + ] + }, + "hash": "9b2052904dc1f773809600a88eb61bb8164b8013f7e2c4b6c2db15077e554cb1" +} diff --git a/lynx/.sqlx/query-9d4f7db0d69ff3e9cbefd6fada39126028db165f45aa6c97ccf0f30d79b6ebf0.json b/lynx/.sqlx/query-9d4f7db0d69ff3e9cbefd6fada39126028db165f45aa6c97ccf0f30d79b6ebf0.json new file mode 100644 index 0000000..141d089 --- /dev/null +++ b/lynx/.sqlx/query-9d4f7db0d69ff3e9cbefd6fada39126028db165f45aa6c97ccf0f30d79b6ebf0.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT COUNT(DISTINCT ur.user_id) FROM user_roles ur\n JOIN role_permissions rp ON rp.role_id = ur.role_id\n JOIN permissions p ON p.id = rp.permission_id\n WHERE ur.role_id = $1 AND p.key = '*:*'", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "count", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [ + null + ] + }, + "hash": "9d4f7db0d69ff3e9cbefd6fada39126028db165f45aa6c97ccf0f30d79b6ebf0" +} diff --git a/lynx/.sqlx/query-9fdee5b0e9b25ae3419bf77b8c2d4427dfa8929da5343a0c116dd486828e1faa.json b/lynx/.sqlx/query-9fdee5b0e9b25ae3419bf77b8c2d4427dfa8929da5343a0c116dd486828e1faa.json new file mode 100644 index 0000000..b860840 --- /dev/null +++ b/lynx/.sqlx/query-9fdee5b0e9b25ae3419bf77b8c2d4427dfa8929da5343a0c116dd486828e1faa.json @@ -0,0 +1,15 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE update_log SET status = $1 WHERE id = $2", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Text", + "Uuid" + ] + }, + "nullable": [] + }, + "hash": "9fdee5b0e9b25ae3419bf77b8c2d4427dfa8929da5343a0c116dd486828e1faa" +} diff --git a/lynx/.sqlx/query-a101f3e73dc4bac366a836b7ff667cc7347b48abd4464732076646980b5d330a.json b/lynx/.sqlx/query-a101f3e73dc4bac366a836b7ff667cc7347b48abd4464732076646980b5d330a.json new file mode 100644 index 0000000..f02ce0f --- /dev/null +++ b/lynx/.sqlx/query-a101f3e73dc4bac366a836b7ff667cc7347b48abd4464732076646980b5d330a.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT version FROM agents WHERE id = $1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "version", + "type_info": "Text" + } + ], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [ + true + ] + }, + "hash": "a101f3e73dc4bac366a836b7ff667cc7347b48abd4464732076646980b5d330a" +} diff --git a/lynx/.sqlx/query-a1cdd2d0e616783625de8cf307a359b8757df8911afaff0358e97b8b7769bf41.json b/lynx/.sqlx/query-a1cdd2d0e616783625de8cf307a359b8757df8911afaff0358e97b8b7769bf41.json new file mode 100644 index 0000000..b12d97f --- /dev/null +++ b/lynx/.sqlx/query-a1cdd2d0e616783625de8cf307a359b8757df8911afaff0358e97b8b7769bf41.json @@ -0,0 +1,49 @@ +{ + "db_name": "PostgreSQL", + "query": "\n WITH new_org AS (\n INSERT INTO organizations (id, name, slug, owner_id)\n VALUES ($1, $2, $3, $4)\n RETURNING *\n ),\n _ AS (\n INSERT INTO organization_members (organization_id, user_id, role)\n VALUES ($1, $4, 'owner')\n )\n SELECT id, name, slug, owner_id, created_at FROM new_org\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "name", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "slug", + "type_info": "Text" + }, + { + "ordinal": 3, + "name": "owner_id", + "type_info": "Uuid" + }, + { + "ordinal": 4, + "name": "created_at", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [ + "Uuid", + "Text", + "Text", + "Uuid" + ] + }, + "nullable": [ + false, + false, + false, + false, + false + ] + }, + "hash": "a1cdd2d0e616783625de8cf307a359b8757df8911afaff0358e97b8b7769bf41" +} diff --git a/lynx/.sqlx/query-a1ff2c0400bd15bd04e56cad71e6fd7948dec918bf1374fe9522f239e680d686.json b/lynx/.sqlx/query-a1ff2c0400bd15bd04e56cad71e6fd7948dec918bf1374fe9522f239e680d686.json new file mode 100644 index 0000000..0cda45f --- /dev/null +++ b/lynx/.sqlx/query-a1ff2c0400bd15bd04e56cad71e6fd7948dec918bf1374fe9522f239e680d686.json @@ -0,0 +1,14 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE security_alerts SET acknowledged_at = NOW() WHERE id = $1", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [] + }, + "hash": "a1ff2c0400bd15bd04e56cad71e6fd7948dec918bf1374fe9522f239e680d686" +} diff --git a/lynx/.sqlx/query-a4356813f382dca9f97f38b504e7285fc77e79296e143e7c8b54c299e175b066.json b/lynx/.sqlx/query-a4356813f382dca9f97f38b504e7285fc77e79296e143e7c8b54c299e175b066.json new file mode 100644 index 0000000..d78953d --- /dev/null +++ b/lynx/.sqlx/query-a4356813f382dca9f97f38b504e7285fc77e79296e143e7c8b54c299e175b066.json @@ -0,0 +1,14 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE migration_state SET status='error', error_message=$1, updated_at=NOW() WHERE id=1", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [] + }, + "hash": "a4356813f382dca9f97f38b504e7285fc77e79296e143e7c8b54c299e175b066" +} diff --git a/lynx/.sqlx/query-a70e3f0396f786f2559abd657a1f627c3b18973a8ac08b85290d8e6061a0f244.json b/lynx/.sqlx/query-a70e3f0396f786f2559abd657a1f627c3b18973a8ac08b85290d8e6061a0f244.json new file mode 100644 index 0000000..05d0469 --- /dev/null +++ b/lynx/.sqlx/query-a70e3f0396f786f2559abd657a1f627c3b18973a8ac08b85290d8e6061a0f244.json @@ -0,0 +1,14 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE domain_config SET status='error', error_message=$1, updated_at=NOW() WHERE id=1", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [] + }, + "hash": "a70e3f0396f786f2559abd657a1f627c3b18973a8ac08b85290d8e6061a0f244" +} diff --git a/lynx/.sqlx/query-a8b72f2a5ffcc619153b52dd5fa49dd9e88545c22e267ceb680a94c090fbc739.json b/lynx/.sqlx/query-a8b72f2a5ffcc619153b52dd5fa49dd9e88545c22e267ceb680a94c090fbc739.json new file mode 100644 index 0000000..1b220f7 --- /dev/null +++ b/lynx/.sqlx/query-a8b72f2a5ffcc619153b52dd5fa49dd9e88545c22e267ceb680a94c090fbc739.json @@ -0,0 +1,20 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT ip::text AS ip FROM ip_pool WHERE agent_id IS NULL ORDER BY ip LIMIT 1 FOR UPDATE SKIP LOCKED", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "ip", + "type_info": "Text" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + null + ] + }, + "hash": "a8b72f2a5ffcc619153b52dd5fa49dd9e88545c22e267ceb680a94c090fbc739" +} diff --git a/lynx/.sqlx/query-aa4a6f44dcfa7269ffc4c6bbf690a85cf44e28f547071dd8147da5191a8a41fb.json b/lynx/.sqlx/query-aa4a6f44dcfa7269ffc4c6bbf690a85cf44e28f547071dd8147da5191a8a41fb.json new file mode 100644 index 0000000..5a432bc --- /dev/null +++ b/lynx/.sqlx/query-aa4a6f44dcfa7269ffc4c6bbf690a85cf44e28f547071dd8147da5191a8a41fb.json @@ -0,0 +1,17 @@ +{ + "db_name": "PostgreSQL", + "query": "\n INSERT INTO update_log (id, triggered_by, version, channel, scope, agent_id, status)\n VALUES ($1, NULL, $2, 'stable', 'agent', $3, $4)\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid", + "Text", + "Uuid", + "Text" + ] + }, + "nullable": [] + }, + "hash": "aa4a6f44dcfa7269ffc4c6bbf690a85cf44e28f547071dd8147da5191a8a41fb" +} diff --git a/lynx/.sqlx/query-ac06ef92f76ab45c3d54f5cf17ea36906062db80a2951acfff6ceac849643ad3.json b/lynx/.sqlx/query-ac06ef92f76ab45c3d54f5cf17ea36906062db80a2951acfff6ceac849643ad3.json new file mode 100644 index 0000000..e4ba998 --- /dev/null +++ b/lynx/.sqlx/query-ac06ef92f76ab45c3d54f5cf17ea36906062db80a2951acfff6ceac849643ad3.json @@ -0,0 +1,130 @@ +{ + "db_name": "PostgreSQL", + "query": "\n INSERT INTO nftables_rules\n (id, scope, agent_id, kind, port, port_end, protocol, ip_list, ip_version,\n rate_per_min, description, priority, direction, created_by)\n VALUES ($1, 'local', $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)\n RETURNING id, scope, agent_id, kind, port, port_end, protocol,\n ip_list, ip_version, rate_per_min, description,\n priority, enabled, direction, created_by, created_at, updated_at\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "scope", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "agent_id", + "type_info": "Uuid" + }, + { + "ordinal": 3, + "name": "kind", + "type_info": "Text" + }, + { + "ordinal": 4, + "name": "port", + "type_info": "Int4" + }, + { + "ordinal": 5, + "name": "port_end", + "type_info": "Int4" + }, + { + "ordinal": 6, + "name": "protocol", + "type_info": "Text" + }, + { + "ordinal": 7, + "name": "ip_list", + "type_info": "TextArray" + }, + { + "ordinal": 8, + "name": "ip_version", + "type_info": "Text" + }, + { + "ordinal": 9, + "name": "rate_per_min", + "type_info": "Int4" + }, + { + "ordinal": 10, + "name": "description", + "type_info": "Text" + }, + { + "ordinal": 11, + "name": "priority", + "type_info": "Int4" + }, + { + "ordinal": 12, + "name": "enabled", + "type_info": "Bool" + }, + { + "ordinal": 13, + "name": "direction", + "type_info": "Text" + }, + { + "ordinal": 14, + "name": "created_by", + "type_info": "Uuid" + }, + { + "ordinal": 15, + "name": "created_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 16, + "name": "updated_at", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [ + "Uuid", + "Uuid", + "Text", + "Int4", + "Int4", + "Text", + "TextArray", + "Text", + "Int4", + "Text", + "Int4", + "Text", + "Uuid" + ] + }, + "nullable": [ + false, + false, + true, + false, + true, + true, + true, + false, + false, + true, + true, + false, + false, + false, + true, + false, + false + ] + }, + "hash": "ac06ef92f76ab45c3d54f5cf17ea36906062db80a2951acfff6ceac849643ad3" +} diff --git a/lynx/.sqlx/query-ad2c4af6faa02aff176a4fe887aca9937dc65d2faa7200997769d20944b408d8.json b/lynx/.sqlx/query-ad2c4af6faa02aff176a4fe887aca9937dc65d2faa7200997769d20944b408d8.json new file mode 100644 index 0000000..de785a2 --- /dev/null +++ b/lynx/.sqlx/query-ad2c4af6faa02aff176a4fe887aca9937dc65d2faa7200997769d20944b408d8.json @@ -0,0 +1,38 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT id, username, force_password_change, created_at FROM users ORDER BY created_at", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "username", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "force_password_change", + "type_info": "Bool" + }, + { + "ordinal": 3, + "name": "created_at", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + false, + false, + false, + false + ] + }, + "hash": "ad2c4af6faa02aff176a4fe887aca9937dc65d2faa7200997769d20944b408d8" +} diff --git a/lynx/.sqlx/query-b2dfe766582aaba5f1dc08037546337c5aadcf8d596718669238bfea645e3ea0.json b/lynx/.sqlx/query-b2dfe766582aaba5f1dc08037546337c5aadcf8d596718669238bfea645e3ea0.json new file mode 100644 index 0000000..32b98a7 --- /dev/null +++ b/lynx/.sqlx/query-b2dfe766582aaba5f1dc08037546337c5aadcf8d596718669238bfea645e3ea0.json @@ -0,0 +1,68 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT id, domain, cert_type, cert_expires_at, hsts_enabled, port_19443_open, status, error_message, updated_at FROM domain_config WHERE id = 1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int4" + }, + { + "ordinal": 1, + "name": "domain", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "cert_type", + "type_info": "Text" + }, + { + "ordinal": 3, + "name": "cert_expires_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 4, + "name": "hsts_enabled", + "type_info": "Bool" + }, + { + "ordinal": 5, + "name": "port_19443_open", + "type_info": "Bool" + }, + { + "ordinal": 6, + "name": "status", + "type_info": "Text" + }, + { + "ordinal": 7, + "name": "error_message", + "type_info": "Text" + }, + { + "ordinal": 8, + "name": "updated_at", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + false, + true, + false, + true, + false, + false, + false, + true, + false + ] + }, + "hash": "b2dfe766582aaba5f1dc08037546337c5aadcf8d596718669238bfea645e3ea0" +} diff --git a/lynx/.sqlx/query-b4680e78db63851fa00f8695dfbe7a868c543256e6401640de0d16165e330ad7.json b/lynx/.sqlx/query-b4680e78db63851fa00f8695dfbe7a868c543256e6401640de0d16165e330ad7.json new file mode 100644 index 0000000..1bc6a59 --- /dev/null +++ b/lynx/.sqlx/query-b4680e78db63851fa00f8695dfbe7a868c543256e6401640de0d16165e330ad7.json @@ -0,0 +1,20 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT id FROM agents WHERE status != 'online'", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + false + ] + }, + "hash": "b4680e78db63851fa00f8695dfbe7a868c543256e6401640de0d16165e330ad7" +} diff --git a/lynx/.sqlx/query-b48607b49c20e23f376065a682ab2b09e209f3d286b71130b3f2ece50c5fb78f.json b/lynx/.sqlx/query-b48607b49c20e23f376065a682ab2b09e209f3d286b71130b3f2ece50c5fb78f.json new file mode 100644 index 0000000..b3a6df0 --- /dev/null +++ b/lynx/.sqlx/query-b48607b49c20e23f376065a682ab2b09e209f3d286b71130b3f2ece50c5fb78f.json @@ -0,0 +1,20 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT entry_hash FROM audit_log ORDER BY created_at DESC LIMIT 1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "entry_hash", + "type_info": "Text" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + false + ] + }, + "hash": "b48607b49c20e23f376065a682ab2b09e209f3d286b71130b3f2ece50c5fb78f" +} diff --git a/lynx/.sqlx/query-b7136c6331f8ec94862950f35c109bca6fbae8e1f09099143cc75273b171ebef.json b/lynx/.sqlx/query-b7136c6331f8ec94862950f35c109bca6fbae8e1f09099143cc75273b171ebef.json new file mode 100644 index 0000000..4b5c598 --- /dev/null +++ b/lynx/.sqlx/query-b7136c6331f8ec94862950f35c109bca6fbae8e1f09099143cc75273b171ebef.json @@ -0,0 +1,15 @@ +{ + "db_name": "PostgreSQL", + "query": "\n INSERT INTO update_log (id, triggered_by, version, channel, scope, agent_id, status)\n VALUES ($1, NULL, $2, 'stable', 'dashboard', NULL, 'pending')\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid", + "Text" + ] + }, + "nullable": [] + }, + "hash": "b7136c6331f8ec94862950f35c109bca6fbae8e1f09099143cc75273b171ebef" +} diff --git a/lynx/.sqlx/query-b721a02127fa5d53ac29fd661d789abcc317357595a6a838fa590ae2d9cabd53.json b/lynx/.sqlx/query-b721a02127fa5d53ac29fd661d789abcc317357595a6a838fa590ae2d9cabd53.json new file mode 100644 index 0000000..39f3ced --- /dev/null +++ b/lynx/.sqlx/query-b721a02127fa5d53ac29fd661d789abcc317357595a6a838fa590ae2d9cabd53.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT wg_pubkey FROM agents WHERE id = $1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "wg_pubkey", + "type_info": "Text" + } + ], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [ + false + ] + }, + "hash": "b721a02127fa5d53ac29fd661d789abcc317357595a6a838fa590ae2d9cabd53" +} diff --git a/lynx/.sqlx/query-bcc7d725722d902786058ed6a2d436332ce30ec7961da355f33c3fb752d07059.json b/lynx/.sqlx/query-bcc7d725722d902786058ed6a2d436332ce30ec7961da355f33c3fb752d07059.json new file mode 100644 index 0000000..ab3663a --- /dev/null +++ b/lynx/.sqlx/query-bcc7d725722d902786058ed6a2d436332ce30ec7961da355f33c3fb752d07059.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT theme FROM user_preferences WHERE user_id = $1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "theme", + "type_info": "Text" + } + ], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [ + false + ] + }, + "hash": "bcc7d725722d902786058ed6a2d436332ce30ec7961da355f33c3fb752d07059" +} diff --git a/lynx/.sqlx/query-bce32ae0cb0f46b91f0d47a5255a1d56d776ea83bff7c764b125301326068162.json b/lynx/.sqlx/query-bce32ae0cb0f46b91f0d47a5255a1d56d776ea83bff7c764b125301326068162.json new file mode 100644 index 0000000..9e34659 --- /dev/null +++ b/lynx/.sqlx/query-bce32ae0cb0f46b91f0d47a5255a1d56d776ea83bff7c764b125301326068162.json @@ -0,0 +1,14 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE agents SET status='offline' WHERE id=$1", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [] + }, + "hash": "bce32ae0cb0f46b91f0d47a5255a1d56d776ea83bff7c764b125301326068162" +} diff --git a/lynx/.sqlx/query-bcf8f8546e55c422a00a2a0fe13ce803fc87f10716559b6dc7e6ea061aaecb8c.json b/lynx/.sqlx/query-bcf8f8546e55c422a00a2a0fe13ce803fc87f10716559b6dc7e6ea061aaecb8c.json new file mode 100644 index 0000000..252663c --- /dev/null +++ b/lynx/.sqlx/query-bcf8f8546e55c422a00a2a0fe13ce803fc87f10716559b6dc7e6ea061aaecb8c.json @@ -0,0 +1,26 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT domain, status FROM domain_config WHERE id = 1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "domain", + "type_info": "Text" + }, + { + "ordinal": 1, + "name": "status", + "type_info": "Text" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + true, + false + ] + }, + "hash": "bcf8f8546e55c422a00a2a0fe13ce803fc87f10716559b6dc7e6ea061aaecb8c" +} diff --git a/lynx/.sqlx/query-be120dfdb84bf3d2a4aaa0f8304336a5cde84461ac2782323a9efa8a5fc7478d.json b/lynx/.sqlx/query-be120dfdb84bf3d2a4aaa0f8304336a5cde84461ac2782323a9efa8a5fc7478d.json new file mode 100644 index 0000000..89f9496 --- /dev/null +++ b/lynx/.sqlx/query-be120dfdb84bf3d2a4aaa0f8304336a5cde84461ac2782323a9efa8a5fc7478d.json @@ -0,0 +1,20 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT COUNT(DISTINCT ur.user_id) FROM user_roles ur\n JOIN role_permissions rp ON rp.role_id = ur.role_id\n JOIN permissions p ON p.id = rp.permission_id\n WHERE p.key = '*:*'", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "count", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + null + ] + }, + "hash": "be120dfdb84bf3d2a4aaa0f8304336a5cde84461ac2782323a9efa8a5fc7478d" +} diff --git a/lynx/.sqlx/query-be3842946fe4e2eab3395bf6ac5385cbef60f14f19815da84ecbae6285450598.json b/lynx/.sqlx/query-be3842946fe4e2eab3395bf6ac5385cbef60f14f19815da84ecbae6285450598.json new file mode 100644 index 0000000..49e2a0b --- /dev/null +++ b/lynx/.sqlx/query-be3842946fe4e2eab3395bf6ac5385cbef60f14f19815da84ecbae6285450598.json @@ -0,0 +1,15 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO agent_events (id, agent_id, event, detail) VALUES ($1, $2, 'connected', NULL)", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid", + "Uuid" + ] + }, + "nullable": [] + }, + "hash": "be3842946fe4e2eab3395bf6ac5385cbef60f14f19815da84ecbae6285450598" +} diff --git a/lynx/.sqlx/query-bf0829f55035d5c0c9ecc522dde8bf270cda957987d5609d3682eb9179a633e3.json b/lynx/.sqlx/query-bf0829f55035d5c0c9ecc522dde8bf270cda957987d5609d3682eb9179a633e3.json new file mode 100644 index 0000000..89b7181 --- /dev/null +++ b/lynx/.sqlx/query-bf0829f55035d5c0c9ecc522dde8bf270cda957987d5609d3682eb9179a633e3.json @@ -0,0 +1,16 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE agents SET cert_payload = $1, cert_signature = $2, cert_expires_at = NOW() + INTERVAL '90 days' WHERE id = $3", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Text", + "Text", + "Uuid" + ] + }, + "nullable": [] + }, + "hash": "bf0829f55035d5c0c9ecc522dde8bf270cda957987d5609d3682eb9179a633e3" +} diff --git a/lynx/.sqlx/query-bf8a29d946b3ce005da228ea2b24ffa0e0577e8759bc47e58f042c55e27a4af3.json b/lynx/.sqlx/query-bf8a29d946b3ce005da228ea2b24ffa0e0577e8759bc47e58f042c55e27a4af3.json new file mode 100644 index 0000000..5d48037 --- /dev/null +++ b/lynx/.sqlx/query-bf8a29d946b3ce005da228ea2b24ffa0e0577e8759bc47e58f042c55e27a4af3.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT 1 FROM global_rule_sync WHERE agent_id = $1 AND synced_at IS NULL LIMIT 1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "?column?", + "type_info": "Int4" + } + ], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [ + null + ] + }, + "hash": "bf8a29d946b3ce005da228ea2b24ffa0e0577e8759bc47e58f042c55e27a4af3" +} diff --git a/lynx/.sqlx/query-c0528d6051f201cbd154994d8baf135cb20ecc018c2204f642674c1da8cafacb.json b/lynx/.sqlx/query-c0528d6051f201cbd154994d8baf135cb20ecc018c2204f642674c1da8cafacb.json new file mode 100644 index 0000000..593e727 --- /dev/null +++ b/lynx/.sqlx/query-c0528d6051f201cbd154994d8baf135cb20ecc018c2204f642674c1da8cafacb.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT sync_token_hash FROM agents WHERE id=$1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "sync_token_hash", + "type_info": "Text" + } + ], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [ + true + ] + }, + "hash": "c0528d6051f201cbd154994d8baf135cb20ecc018c2204f642674c1da8cafacb" +} diff --git a/lynx/.sqlx/query-c2ad4ef2a4f50273ad988e02b07b04c81e645e9a41485c7d9a8cb4e5198f2750.json b/lynx/.sqlx/query-c2ad4ef2a4f50273ad988e02b07b04c81e645e9a41485c7d9a8cb4e5198f2750.json new file mode 100644 index 0000000..29169f1 --- /dev/null +++ b/lynx/.sqlx/query-c2ad4ef2a4f50273ad988e02b07b04c81e645e9a41485c7d9a8cb4e5198f2750.json @@ -0,0 +1,15 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE users SET password_hash = $1, force_password_change = FALSE WHERE id = $2", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Text", + "Uuid" + ] + }, + "nullable": [] + }, + "hash": "c2ad4ef2a4f50273ad988e02b07b04c81e645e9a41485c7d9a8cb4e5198f2750" +} diff --git a/lynx/.sqlx/query-c3445902b5d334063f62d3e2c219f9924645e1bfb78df5e52bb66b1d640cd744.json b/lynx/.sqlx/query-c3445902b5d334063f62d3e2c219f9924645e1bfb78df5e52bb66b1d640cd744.json new file mode 100644 index 0000000..8d4b2a0 --- /dev/null +++ b/lynx/.sqlx/query-c3445902b5d334063f62d3e2c219f9924645e1bfb78df5e52bb66b1d640cd744.json @@ -0,0 +1,14 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE migration_state SET status='waiting_agents', agents_total=$1, updated_at=NOW() WHERE id=1", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int4" + ] + }, + "nullable": [] + }, + "hash": "c3445902b5d334063f62d3e2c219f9924645e1bfb78df5e52bb66b1d640cd744" +} diff --git a/lynx/.sqlx/query-c359906fc42e182370256bf302c022374bfc665864fd14da9852556537dd62e3.json b/lynx/.sqlx/query-c359906fc42e182370256bf302c022374bfc665864fd14da9852556537dd62e3.json new file mode 100644 index 0000000..4c41b11 --- /dev/null +++ b/lynx/.sqlx/query-c359906fc42e182370256bf302c022374bfc665864fd14da9852556537dd62e3.json @@ -0,0 +1,44 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT id, kind, detail, agent_id, created_at\n FROM security_alerts\n WHERE acknowledged_at IS NULL\n ORDER BY created_at DESC\n LIMIT 100", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "kind", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "detail", + "type_info": "Text" + }, + { + "ordinal": 3, + "name": "agent_id", + "type_info": "Uuid" + }, + { + "ordinal": 4, + "name": "created_at", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + false, + false, + true, + true, + false + ] + }, + "hash": "c359906fc42e182370256bf302c022374bfc665864fd14da9852556537dd62e3" +} diff --git a/lynx/.sqlx/query-c4f83d35f2074769fea148a0459a24bbe401d91c23501e334b2b6a317886cc9a.json b/lynx/.sqlx/query-c4f83d35f2074769fea148a0459a24bbe401d91c23501e334b2b6a317886cc9a.json new file mode 100644 index 0000000..fb48ce4 --- /dev/null +++ b/lynx/.sqlx/query-c4f83d35f2074769fea148a0459a24bbe401d91c23501e334b2b6a317886cc9a.json @@ -0,0 +1,28 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT p.id, p.key FROM role_permissions rp JOIN permissions p ON p.id = rp.permission_id WHERE rp.role_id = $1 ORDER BY p.key", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "key", + "type_info": "Text" + } + ], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [ + false, + false + ] + }, + "hash": "c4f83d35f2074769fea148a0459a24bbe401d91c23501e334b2b6a317886cc9a" +} diff --git a/lynx/.sqlx/query-c69f4c85dfbc06b1009e4dea65142078f4a7cdb5a0c6145f2a417a0a5e0295b9.json b/lynx/.sqlx/query-c69f4c85dfbc06b1009e4dea65142078f4a7cdb5a0c6145f2a417a0a5e0295b9.json new file mode 100644 index 0000000..47dc4b7 --- /dev/null +++ b/lynx/.sqlx/query-c69f4c85dfbc06b1009e4dea65142078f4a7cdb5a0c6145f2a417a0a5e0295b9.json @@ -0,0 +1,20 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT COUNT(*) FROM data_plane_tunnels", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "count", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + null + ] + }, + "hash": "c69f4c85dfbc06b1009e4dea65142078f4a7cdb5a0c6145f2a417a0a5e0295b9" +} diff --git a/lynx/.sqlx/query-caefed268a457a3df763fe27c395ede9b0179463756eb78c9ad733cd52280e26.json b/lynx/.sqlx/query-caefed268a457a3df763fe27c395ede9b0179463756eb78c9ad733cd52280e26.json new file mode 100644 index 0000000..1f2b4ef --- /dev/null +++ b/lynx/.sqlx/query-caefed268a457a3df763fe27c395ede9b0179463756eb78c9ad733cd52280e26.json @@ -0,0 +1,20 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT value FROM system_config WHERE key = 'setup_token_issued_at'", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "value", + "type_info": "Text" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + false + ] + }, + "hash": "caefed268a457a3df763fe27c395ede9b0179463756eb78c9ad733cd52280e26" +} diff --git a/lynx/.sqlx/query-cde1a0d0f7444e516b041918065ee9249a3dfce2f5c7d9d48cc8365f6c5d36fe.json b/lynx/.sqlx/query-cde1a0d0f7444e516b041918065ee9249a3dfce2f5c7d9d48cc8365f6c5d36fe.json new file mode 100644 index 0000000..d6bd8f0 --- /dev/null +++ b/lynx/.sqlx/query-cde1a0d0f7444e516b041918065ee9249a3dfce2f5c7d9d48cc8365f6c5d36fe.json @@ -0,0 +1,38 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT id, wg_ip, api_port, status FROM agents WHERE status = 'online'", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "wg_ip", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "api_port", + "type_info": "Int4" + }, + { + "ordinal": 3, + "name": "status", + "type_info": "Text" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + false, + false, + false, + false + ] + }, + "hash": "cde1a0d0f7444e516b041918065ee9249a3dfce2f5c7d9d48cc8365f6c5d36fe" +} diff --git a/lynx/.sqlx/query-ce5d92880df07764b51b9ce1869930b72ba1d4d642a607a719c3ac45bb252595.json b/lynx/.sqlx/query-ce5d92880df07764b51b9ce1869930b72ba1d4d642a607a719c3ac45bb252595.json new file mode 100644 index 0000000..d41de56 --- /dev/null +++ b/lynx/.sqlx/query-ce5d92880df07764b51b9ce1869930b72ba1d4d642a607a719c3ac45bb252595.json @@ -0,0 +1,38 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT id, wg_ip, api_port, status FROM agents", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "wg_ip", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "api_port", + "type_info": "Int4" + }, + { + "ordinal": 3, + "name": "status", + "type_info": "Text" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + false, + false, + false, + false + ] + }, + "hash": "ce5d92880df07764b51b9ce1869930b72ba1d4d642a607a719c3ac45bb252595" +} diff --git a/lynx/.sqlx/query-cf1514bfec20e40e19443a39e371d2792cafb0b80fea5a8ca7df57ea3c8f6e7f.json b/lynx/.sqlx/query-cf1514bfec20e40e19443a39e371d2792cafb0b80fea5a8ca7df57ea3c8f6e7f.json new file mode 100644 index 0000000..b44e670 --- /dev/null +++ b/lynx/.sqlx/query-cf1514bfec20e40e19443a39e371d2792cafb0b80fea5a8ca7df57ea3c8f6e7f.json @@ -0,0 +1,40 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT id, wg_ip::text AS wg_ip, api_port, COALESCE(arch, 'x86_64') AS arch FROM agents WHERE status = 'online' AND (version IS NULL OR version != $1)", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "wg_ip", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "api_port", + "type_info": "Int4" + }, + { + "ordinal": 3, + "name": "arch", + "type_info": "Text" + } + ], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [ + false, + false, + false, + null + ] + }, + "hash": "cf1514bfec20e40e19443a39e371d2792cafb0b80fea5a8ca7df57ea3c8f6e7f" +} diff --git a/lynx/.sqlx/query-cf86def10b30f5d8a43d5847fb8699b53f5f8f0a78f5765028c93d29434a2143.json b/lynx/.sqlx/query-cf86def10b30f5d8a43d5847fb8699b53f5f8f0a78f5765028c93d29434a2143.json new file mode 100644 index 0000000..86e5be7 --- /dev/null +++ b/lynx/.sqlx/query-cf86def10b30f5d8a43d5847fb8699b53f5f8f0a78f5765028c93d29434a2143.json @@ -0,0 +1,20 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT wg_pubkey FROM agents", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "wg_pubkey", + "type_info": "Text" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + false + ] + }, + "hash": "cf86def10b30f5d8a43d5847fb8699b53f5f8f0a78f5765028c93d29434a2143" +} diff --git a/lynx/.sqlx/query-d1153950633136e4f1c754e26e50f89f192c3d122ace0f02e46856a6d92dd7fb.json b/lynx/.sqlx/query-d1153950633136e4f1c754e26e50f89f192c3d122ace0f02e46856a6d92dd7fb.json new file mode 100644 index 0000000..2a227da --- /dev/null +++ b/lynx/.sqlx/query-d1153950633136e4f1c754e26e50f89f192c3d122ace0f02e46856a6d92dd7fb.json @@ -0,0 +1,34 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT id, wg_ip, api_port\n FROM agents\n WHERE status = 'online'\n AND (cert_expires_at IS NULL OR cert_expires_at < NOW() + ($1 || ' days')::INTERVAL)\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "wg_ip", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "api_port", + "type_info": "Int4" + } + ], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [ + false, + false, + false + ] + }, + "hash": "d1153950633136e4f1c754e26e50f89f192c3d122ace0f02e46856a6d92dd7fb" +} diff --git a/lynx/.sqlx/query-d3d8affd6da972e5b1b0502d4687745a98097b59965f5960584f0c2d3c74d3e8.json b/lynx/.sqlx/query-d3d8affd6da972e5b1b0502d4687745a98097b59965f5960584f0c2d3c74d3e8.json new file mode 100644 index 0000000..acf7765 --- /dev/null +++ b/lynx/.sqlx/query-d3d8affd6da972e5b1b0502d4687745a98097b59965f5960584f0c2d3c74d3e8.json @@ -0,0 +1,23 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT 1 FROM organization_members WHERE organization_id = $1 AND user_id = $2", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "?column?", + "type_info": "Int4" + } + ], + "parameters": { + "Left": [ + "Uuid", + "Uuid" + ] + }, + "nullable": [ + null + ] + }, + "hash": "d3d8affd6da972e5b1b0502d4687745a98097b59965f5960584f0c2d3c74d3e8" +} diff --git a/lynx/.sqlx/query-d6fe7969e280a11aafa7c6686ce5f5c37498545f8a11deb3b86f0af2295d91cc.json b/lynx/.sqlx/query-d6fe7969e280a11aafa7c6686ce5f5c37498545f8a11deb3b86f0af2295d91cc.json new file mode 100644 index 0000000..990c73a --- /dev/null +++ b/lynx/.sqlx/query-d6fe7969e280a11aafa7c6686ce5f5c37498545f8a11deb3b86f0af2295d91cc.json @@ -0,0 +1,23 @@ +{ + "db_name": "PostgreSQL", + "query": "DELETE FROM nftables_rules WHERE id = $1 AND scope = 'local' AND agent_id = $2 RETURNING id", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + } + ], + "parameters": { + "Left": [ + "Uuid", + "Uuid" + ] + }, + "nullable": [ + false + ] + }, + "hash": "d6fe7969e280a11aafa7c6686ce5f5c37498545f8a11deb3b86f0af2295d91cc" +} diff --git a/lynx/.sqlx/query-d98c209b07244d0e9ff0be3653026ef23efb0b13214f6e5c7f0423f79142bb6b.json b/lynx/.sqlx/query-d98c209b07244d0e9ff0be3653026ef23efb0b13214f6e5c7f0423f79142bb6b.json new file mode 100644 index 0000000..ab3010e --- /dev/null +++ b/lynx/.sqlx/query-d98c209b07244d0e9ff0be3653026ef23efb0b13214f6e5c7f0423f79142bb6b.json @@ -0,0 +1,77 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT id, agent_id, organization_id, user_id, command_type,\n result, error, previous_hash, entry_hash, created_at\n FROM audit_log\n WHERE created_at > $1\n ORDER BY created_at ASC\n LIMIT $2\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "agent_id", + "type_info": "Uuid" + }, + { + "ordinal": 2, + "name": "organization_id", + "type_info": "Uuid" + }, + { + "ordinal": 3, + "name": "user_id", + "type_info": "Uuid" + }, + { + "ordinal": 4, + "name": "command_type", + "type_info": "Text" + }, + { + "ordinal": 5, + "name": "result", + "type_info": "Text" + }, + { + "ordinal": 6, + "name": "error", + "type_info": "Text" + }, + { + "ordinal": 7, + "name": "previous_hash", + "type_info": "Text" + }, + { + "ordinal": 8, + "name": "entry_hash", + "type_info": "Text" + }, + { + "ordinal": 9, + "name": "created_at", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [ + "Timestamptz", + "Int8" + ] + }, + "nullable": [ + false, + false, + true, + true, + false, + false, + true, + false, + false, + false + ] + }, + "hash": "d98c209b07244d0e9ff0be3653026ef23efb0b13214f6e5c7f0423f79142bb6b" +} diff --git a/lynx/.sqlx/query-d99827a5ef0b4c3de6ac18e84f65a8667184370fb27a2476ce8d2490d072454c.json b/lynx/.sqlx/query-d99827a5ef0b4c3de6ac18e84f65a8667184370fb27a2476ce8d2490d072454c.json new file mode 100644 index 0000000..8433d4a --- /dev/null +++ b/lynx/.sqlx/query-d99827a5ef0b4c3de6ac18e84f65a8667184370fb27a2476ce8d2490d072454c.json @@ -0,0 +1,19 @@ +{ + "db_name": "PostgreSQL", + "query": "\n INSERT INTO users (id, username, email_hash, email_encrypted, password_hash, dek_encrypted)\n VALUES ($1, $2, $3, $4, $5, $6)\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid", + "Text", + "Text", + "Bytea", + "Text", + "Bytea" + ] + }, + "nullable": [] + }, + "hash": "d99827a5ef0b4c3de6ac18e84f65a8667184370fb27a2476ce8d2490d072454c" +} diff --git a/lynx/.sqlx/query-db566c5dfda4e8eafec318a87ee33930d895dddb736c2e5a6e7993cee488d3e7.json b/lynx/.sqlx/query-db566c5dfda4e8eafec318a87ee33930d895dddb736c2e5a6e7993cee488d3e7.json new file mode 100644 index 0000000..4d6ff61 --- /dev/null +++ b/lynx/.sqlx/query-db566c5dfda4e8eafec318a87ee33930d895dddb736c2e5a6e7993cee488d3e7.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "\n INSERT INTO audit_log\n (id, agent_id, organization_id, user_id, command_type, result, error, previous_hash, entry_hash)\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid", + "Uuid", + "Uuid", + "Uuid", + "Text", + "Text", + "Text", + "Text", + "Text" + ] + }, + "nullable": [] + }, + "hash": "db566c5dfda4e8eafec318a87ee33930d895dddb736c2e5a6e7993cee488d3e7" +} diff --git a/lynx/.sqlx/query-dd99e48b1572e25db38f03da95984fda1072913b29bb6b3753a0d351583dfff6.json b/lynx/.sqlx/query-dd99e48b1572e25db38f03da95984fda1072913b29bb6b3753a0d351583dfff6.json new file mode 100644 index 0000000..74b2594 --- /dev/null +++ b/lynx/.sqlx/query-dd99e48b1572e25db38f03da95984fda1072913b29bb6b3753a0d351583dfff6.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT id FROM users WHERE username = $1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + } + ], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [ + false + ] + }, + "hash": "dd99e48b1572e25db38f03da95984fda1072913b29bb6b3753a0d351583dfff6" +} diff --git a/lynx/.sqlx/query-de84ba69402ef3bd9cddb7cf14f4c422305475d42c26eecc4049b08a220ce402.json b/lynx/.sqlx/query-de84ba69402ef3bd9cddb7cf14f4c422305475d42c26eecc4049b08a220ce402.json new file mode 100644 index 0000000..5dd60e4 --- /dev/null +++ b/lynx/.sqlx/query-de84ba69402ef3bd9cddb7cf14f4c422305475d42c26eecc4049b08a220ce402.json @@ -0,0 +1,14 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE data_plane_tunnels SET status='torn_down', updated_at=NOW() WHERE id=$1", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [] + }, + "hash": "de84ba69402ef3bd9cddb7cf14f4c422305475d42c26eecc4049b08a220ce402" +} diff --git a/lynx/.sqlx/query-dfd03e46dec4bd17decd27af0f5e06cbc8b69fc8b29889638f94bad1b7418365.json b/lynx/.sqlx/query-dfd03e46dec4bd17decd27af0f5e06cbc8b69fc8b29889638f94bad1b7418365.json new file mode 100644 index 0000000..a7f27f4 --- /dev/null +++ b/lynx/.sqlx/query-dfd03e46dec4bd17decd27af0f5e06cbc8b69fc8b29889638f94bad1b7418365.json @@ -0,0 +1,116 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT id, scope, agent_id, kind, port, port_end, protocol,\n ip_list, ip_version, rate_per_min, description,\n priority, enabled, direction, created_by, created_at, updated_at\n FROM nftables_rules\n WHERE scope = 'global' AND enabled = true\n ORDER BY priority ASC, created_at ASC\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "scope", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "agent_id", + "type_info": "Uuid" + }, + { + "ordinal": 3, + "name": "kind", + "type_info": "Text" + }, + { + "ordinal": 4, + "name": "port", + "type_info": "Int4" + }, + { + "ordinal": 5, + "name": "port_end", + "type_info": "Int4" + }, + { + "ordinal": 6, + "name": "protocol", + "type_info": "Text" + }, + { + "ordinal": 7, + "name": "ip_list", + "type_info": "TextArray" + }, + { + "ordinal": 8, + "name": "ip_version", + "type_info": "Text" + }, + { + "ordinal": 9, + "name": "rate_per_min", + "type_info": "Int4" + }, + { + "ordinal": 10, + "name": "description", + "type_info": "Text" + }, + { + "ordinal": 11, + "name": "priority", + "type_info": "Int4" + }, + { + "ordinal": 12, + "name": "enabled", + "type_info": "Bool" + }, + { + "ordinal": 13, + "name": "direction", + "type_info": "Text" + }, + { + "ordinal": 14, + "name": "created_by", + "type_info": "Uuid" + }, + { + "ordinal": 15, + "name": "created_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 16, + "name": "updated_at", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + false, + false, + true, + false, + true, + true, + true, + false, + false, + true, + true, + false, + false, + false, + true, + false, + false + ] + }, + "hash": "dfd03e46dec4bd17decd27af0f5e06cbc8b69fc8b29889638f94bad1b7418365" +} diff --git a/lynx/.sqlx/query-dffde846539a7ccf1a63cc2378f4f5d1ed7d60c34d074642e34a6970d3972a13.json b/lynx/.sqlx/query-dffde846539a7ccf1a63cc2378f4f5d1ed7d60c34d074642e34a6970d3972a13.json new file mode 100644 index 0000000..77cc6b1 --- /dev/null +++ b/lynx/.sqlx/query-dffde846539a7ccf1a63cc2378f4f5d1ed7d60c34d074642e34a6970d3972a13.json @@ -0,0 +1,46 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT id, agent_id, event, detail, created_at\n FROM agent_events\n ORDER BY created_at DESC\n LIMIT $1\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "agent_id", + "type_info": "Uuid" + }, + { + "ordinal": 2, + "name": "event", + "type_info": "Text" + }, + { + "ordinal": 3, + "name": "detail", + "type_info": "Text" + }, + { + "ordinal": 4, + "name": "created_at", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [ + false, + false, + false, + true, + false + ] + }, + "hash": "dffde846539a7ccf1a63cc2378f4f5d1ed7d60c34d074642e34a6970d3972a13" +} diff --git a/lynx/.sqlx/query-e0343546b4d839bf052229dcdafb90d801a3ca916849a363869bba605b648b07.json b/lynx/.sqlx/query-e0343546b4d839bf052229dcdafb90d801a3ca916849a363869bba605b648b07.json new file mode 100644 index 0000000..e663772 --- /dev/null +++ b/lynx/.sqlx/query-e0343546b4d839bf052229dcdafb90d801a3ca916849a363869bba605b648b07.json @@ -0,0 +1,15 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE migration_state\n SET status='transferring', role='source', target_url=$1,\n agents_total=$2, agents_confirmed=0,\n started_at=NOW(), updated_at=NOW()\n WHERE id=1", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Text", + "Int4" + ] + }, + "nullable": [] + }, + "hash": "e0343546b4d839bf052229dcdafb90d801a3ca916849a363869bba605b648b07" +} diff --git a/lynx/.sqlx/query-e0458a70ef71f355e2afae88227d0e53b4c11b14dadb199323c766c6de8e9dd2.json b/lynx/.sqlx/query-e0458a70ef71f355e2afae88227d0e53b4c11b14dadb199323c766c6de8e9dd2.json new file mode 100644 index 0000000..bc96739 --- /dev/null +++ b/lynx/.sqlx/query-e0458a70ef71f355e2afae88227d0e53b4c11b14dadb199323c766c6de8e9dd2.json @@ -0,0 +1,23 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT role FROM organization_members WHERE organization_id = $1 AND user_id = $2", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "role", + "type_info": "Text" + } + ], + "parameters": { + "Left": [ + "Uuid", + "Uuid" + ] + }, + "nullable": [ + false + ] + }, + "hash": "e0458a70ef71f355e2afae88227d0e53b4c11b14dadb199323c766c6de8e9dd2" +} diff --git a/lynx/.sqlx/query-e0c30c23263f4228d4fdd5f06c949d0dd2e6d6584eb51c00e191e70404e4915e.json b/lynx/.sqlx/query-e0c30c23263f4228d4fdd5f06c949d0dd2e6d6584eb51c00e191e70404e4915e.json new file mode 100644 index 0000000..13fb194 --- /dev/null +++ b/lynx/.sqlx/query-e0c30c23263f4228d4fdd5f06c949d0dd2e6d6584eb51c00e191e70404e4915e.json @@ -0,0 +1,76 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT id, name, wg_pubkey, wg_ip, wg_endpoint, api_port, status, version, last_heartbeat, created_at FROM agents WHERE id = $1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "name", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "wg_pubkey", + "type_info": "Text" + }, + { + "ordinal": 3, + "name": "wg_ip", + "type_info": "Text" + }, + { + "ordinal": 4, + "name": "wg_endpoint", + "type_info": "Text" + }, + { + "ordinal": 5, + "name": "api_port", + "type_info": "Int4" + }, + { + "ordinal": 6, + "name": "status", + "type_info": "Text" + }, + { + "ordinal": 7, + "name": "version", + "type_info": "Text" + }, + { + "ordinal": 8, + "name": "last_heartbeat", + "type_info": "Timestamptz" + }, + { + "ordinal": 9, + "name": "created_at", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [ + false, + false, + false, + false, + true, + false, + false, + true, + true, + false + ] + }, + "hash": "e0c30c23263f4228d4fdd5f06c949d0dd2e6d6584eb51c00e191e70404e4915e" +} diff --git a/lynx/.sqlx/query-e0e32c83a767ed536f34a71493e3d4b195a7b80662e7cfe54c56d63ba2f731a9.json b/lynx/.sqlx/query-e0e32c83a767ed536f34a71493e3d4b195a7b80662e7cfe54c56d63ba2f731a9.json new file mode 100644 index 0000000..11af083 --- /dev/null +++ b/lynx/.sqlx/query-e0e32c83a767ed536f34a71493e3d4b195a7b80662e7cfe54c56d63ba2f731a9.json @@ -0,0 +1,17 @@ +{ + "db_name": "PostgreSQL", + "query": "\n UPDATE sessions\n SET refresh_token_hash = $1, last_used_at = NOW(), last_jti = $4\n WHERE id = $2 AND refresh_token_hash = $3\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Text", + "Uuid", + "Text", + "Uuid" + ] + }, + "nullable": [] + }, + "hash": "e0e32c83a767ed536f34a71493e3d4b195a7b80662e7cfe54c56d63ba2f731a9" +} diff --git a/lynx/.sqlx/query-e23e31841c153d6358592d735535b4adc86eef2d73058da925e127886b4049bd.json b/lynx/.sqlx/query-e23e31841c153d6358592d735535b4adc86eef2d73058da925e127886b4049bd.json new file mode 100644 index 0000000..b40bbad --- /dev/null +++ b/lynx/.sqlx/query-e23e31841c153d6358592d735535b4adc86eef2d73058da925e127886b4049bd.json @@ -0,0 +1,20 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT status FROM migration_state WHERE id=1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "status", + "type_info": "Text" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + false + ] + }, + "hash": "e23e31841c153d6358592d735535b4adc86eef2d73058da925e127886b4049bd" +} diff --git a/lynx/.sqlx/query-e37a101eba72ca88463656ac48cfcc0528b1b78e3f6bf8839ddb42ed05af1fe7.json b/lynx/.sqlx/query-e37a101eba72ca88463656ac48cfcc0528b1b78e3f6bf8839ddb42ed05af1fe7.json new file mode 100644 index 0000000..ba65441 --- /dev/null +++ b/lynx/.sqlx/query-e37a101eba72ca88463656ac48cfcc0528b1b78e3f6bf8839ddb42ed05af1fe7.json @@ -0,0 +1,28 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT username, single_session FROM users WHERE id = $1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "username", + "type_info": "Text" + }, + { + "ordinal": 1, + "name": "single_session", + "type_info": "Bool" + } + ], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [ + false, + false + ] + }, + "hash": "e37a101eba72ca88463656ac48cfcc0528b1b78e3f6bf8839ddb42ed05af1fe7" +} diff --git a/lynx/.sqlx/query-e42b60278b57d4d0c49bd78fd17862019b6c025124453611dabed9c8b589484d.json b/lynx/.sqlx/query-e42b60278b57d4d0c49bd78fd17862019b6c025124453611dabed9c8b589484d.json new file mode 100644 index 0000000..fdbaef5 --- /dev/null +++ b/lynx/.sqlx/query-e42b60278b57d4d0c49bd78fd17862019b6c025124453611dabed9c8b589484d.json @@ -0,0 +1,76 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT id, project_id, agent_a_id, agent_b_id, agent_a_wg_ip, agent_b_wg_ip,\n wg_port, replica_count, status, created_at\n FROM data_plane_tunnels\n WHERE project_id = $1 AND status != 'torn_down'\n ORDER BY created_at ASC", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "project_id", + "type_info": "Uuid" + }, + { + "ordinal": 2, + "name": "agent_a_id", + "type_info": "Uuid" + }, + { + "ordinal": 3, + "name": "agent_b_id", + "type_info": "Uuid" + }, + { + "ordinal": 4, + "name": "agent_a_wg_ip", + "type_info": "Text" + }, + { + "ordinal": 5, + "name": "agent_b_wg_ip", + "type_info": "Text" + }, + { + "ordinal": 6, + "name": "wg_port", + "type_info": "Int4" + }, + { + "ordinal": 7, + "name": "replica_count", + "type_info": "Int4" + }, + { + "ordinal": 8, + "name": "status", + "type_info": "Text" + }, + { + "ordinal": 9, + "name": "created_at", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [ + false, + false, + false, + false, + false, + false, + false, + false, + false, + false + ] + }, + "hash": "e42b60278b57d4d0c49bd78fd17862019b6c025124453611dabed9c8b589484d" +} diff --git a/lynx/.sqlx/query-e9ee477fc969775d4a868a773162a3d14a8bdb38cbdad2069ecea6b100bee629.json b/lynx/.sqlx/query-e9ee477fc969775d4a868a773162a3d14a8bdb38cbdad2069ecea6b100bee629.json new file mode 100644 index 0000000..d2e871a --- /dev/null +++ b/lynx/.sqlx/query-e9ee477fc969775d4a868a773162a3d14a8bdb38cbdad2069ecea6b100bee629.json @@ -0,0 +1,14 @@ +{ + "db_name": "PostgreSQL", + "query": "DELETE FROM sessions WHERE user_id = $1", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [] + }, + "hash": "e9ee477fc969775d4a868a773162a3d14a8bdb38cbdad2069ecea6b100bee629" +} diff --git a/lynx/.sqlx/query-ec380f0eb30d86139ae6c5b773361f9052ac74ed48761317f73e94d5e483797e.json b/lynx/.sqlx/query-ec380f0eb30d86139ae6c5b773361f9052ac74ed48761317f73e94d5e483797e.json new file mode 100644 index 0000000..41945ec --- /dev/null +++ b/lynx/.sqlx/query-ec380f0eb30d86139ae6c5b773361f9052ac74ed48761317f73e94d5e483797e.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT key FROM permissions WHERE id = $1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "key", + "type_info": "Text" + } + ], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [ + false + ] + }, + "hash": "ec380f0eb30d86139ae6c5b773361f9052ac74ed48761317f73e94d5e483797e" +} diff --git a/lynx/.sqlx/query-ec9246d45e3451b65f67d8414ab7a63036970f7cda57b5c17e0b57447963586c.json b/lynx/.sqlx/query-ec9246d45e3451b65f67d8414ab7a63036970f7cda57b5c17e0b57447963586c.json new file mode 100644 index 0000000..fc2585f --- /dev/null +++ b/lynx/.sqlx/query-ec9246d45e3451b65f67d8414ab7a63036970f7cda57b5c17e0b57447963586c.json @@ -0,0 +1,47 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT o.id, o.name, o.slug, o.owner_id, o.created_at\n FROM organizations o\n JOIN organization_members m ON m.organization_id = o.id\n WHERE o.id = $1 AND m.user_id = $2\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "name", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "slug", + "type_info": "Text" + }, + { + "ordinal": 3, + "name": "owner_id", + "type_info": "Uuid" + }, + { + "ordinal": 4, + "name": "created_at", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [ + "Uuid", + "Uuid" + ] + }, + "nullable": [ + false, + false, + false, + false, + false + ] + }, + "hash": "ec9246d45e3451b65f67d8414ab7a63036970f7cda57b5c17e0b57447963586c" +} diff --git a/lynx/.sqlx/query-ef6ac9fe6505a4a987ae6406630167b3caa2b04fc004e62f57d6a5fcdc8bdabe.json b/lynx/.sqlx/query-ef6ac9fe6505a4a987ae6406630167b3caa2b04fc004e62f57d6a5fcdc8bdabe.json new file mode 100644 index 0000000..4212d2b --- /dev/null +++ b/lynx/.sqlx/query-ef6ac9fe6505a4a987ae6406630167b3caa2b04fc004e62f57d6a5fcdc8bdabe.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT COUNT(DISTINCT ur.user_id) FROM user_roles ur WHERE ur.role_id = $1\n AND NOT EXISTS (\n SELECT 1 FROM user_roles ur2\n JOIN role_permissions rp2 ON rp2.role_id = ur2.role_id\n JOIN permissions p2 ON p2.id = rp2.permission_id\n WHERE ur2.user_id = ur.user_id AND ur2.role_id != $1 AND p2.key = '*:*'\n )", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "count", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [ + null + ] + }, + "hash": "ef6ac9fe6505a4a987ae6406630167b3caa2b04fc004e62f57d6a5fcdc8bdabe" +} diff --git a/lynx/.sqlx/query-f13916ea4ced9c5abe7f12e162508b64f3cd51152586dcff67cdd4c1bc18399a.json b/lynx/.sqlx/query-f13916ea4ced9c5abe7f12e162508b64f3cd51152586dcff67cdd4c1bc18399a.json new file mode 100644 index 0000000..c582670 --- /dev/null +++ b/lynx/.sqlx/query-f13916ea4ced9c5abe7f12e162508b64f3cd51152586dcff67cdd4c1bc18399a.json @@ -0,0 +1,34 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT id, detail, created_at\n FROM agent_events\n WHERE agent_id = $1\n AND event = 'nftables_divergence'\n AND created_at > COALESCE(\n (SELECT created_at FROM agent_events\n WHERE agent_id = $1 AND event IN ('nftables_restored', 'nftables_accepted')\n ORDER BY created_at DESC LIMIT 1),\n '1970-01-01'::timestamptz\n )\n ORDER BY created_at DESC\n LIMIT 1\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "detail", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "created_at", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [ + false, + true, + false + ] + }, + "hash": "f13916ea4ced9c5abe7f12e162508b64f3cd51152586dcff67cdd4c1bc18399a" +} diff --git a/lynx/.sqlx/query-f4c6a65036b9705f43dd8a77bb46b2aad4cd543286d76ebd4580b98f6cb92f66.json b/lynx/.sqlx/query-f4c6a65036b9705f43dd8a77bb46b2aad4cd543286d76ebd4580b98f6cb92f66.json new file mode 100644 index 0000000..0756265 --- /dev/null +++ b/lynx/.sqlx/query-f4c6a65036b9705f43dd8a77bb46b2aad4cd543286d76ebd4580b98f6cb92f66.json @@ -0,0 +1,14 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE domain_config SET hsts_enabled=$1, updated_at=NOW() WHERE id=1", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Bool" + ] + }, + "nullable": [] + }, + "hash": "f4c6a65036b9705f43dd8a77bb46b2aad4cd543286d76ebd4580b98f6cb92f66" +} diff --git a/lynx/.sqlx/query-f4cf1b2b88d9cb88a0d2ef8bc6b2e4e48570a40563f7a7e29b863048f426ad15.json b/lynx/.sqlx/query-f4cf1b2b88d9cb88a0d2ef8bc6b2e4e48570a40563f7a7e29b863048f426ad15.json new file mode 100644 index 0000000..3f528d7 --- /dev/null +++ b/lynx/.sqlx/query-f4cf1b2b88d9cb88a0d2ef8bc6b2e4e48570a40563f7a7e29b863048f426ad15.json @@ -0,0 +1,16 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO agent_events (id, agent_id, event, detail) VALUES ($1, $2, 'audit_integrity_failure', $3)", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid", + "Uuid", + "Text" + ] + }, + "nullable": [] + }, + "hash": "f4cf1b2b88d9cb88a0d2ef8bc6b2e4e48570a40563f7a7e29b863048f426ad15" +} diff --git a/lynx/.sqlx/query-f7a980eee80b1db3ebe15d7e2a13540d4f12a1ad6c7d05f04f30a66950eb4e34.json b/lynx/.sqlx/query-f7a980eee80b1db3ebe15d7e2a13540d4f12a1ad6c7d05f04f30a66950eb4e34.json new file mode 100644 index 0000000..4c10d0d --- /dev/null +++ b/lynx/.sqlx/query-f7a980eee80b1db3ebe15d7e2a13540d4f12a1ad6c7d05f04f30a66950eb4e34.json @@ -0,0 +1,32 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT id, wg_ip, api_port FROM agents WHERE status = 'online'", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "wg_ip", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "api_port", + "type_info": "Int4" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + false, + false, + false + ] + }, + "hash": "f7a980eee80b1db3ebe15d7e2a13540d4f12a1ad6c7d05f04f30a66950eb4e34" +} diff --git a/lynx/.sqlx/query-f89ee4d691ccc597f13408f072c47404ca4f5550232cbe81b0437eea3d7441da.json b/lynx/.sqlx/query-f89ee4d691ccc597f13408f072c47404ca4f5550232cbe81b0437eea3d7441da.json new file mode 100644 index 0000000..948c6f1 --- /dev/null +++ b/lynx/.sqlx/query-f89ee4d691ccc597f13408f072c47404ca4f5550232cbe81b0437eea3d7441da.json @@ -0,0 +1,14 @@ +{ + "db_name": "PostgreSQL", + "query": "DELETE FROM agents WHERE id = $1", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [] + }, + "hash": "f89ee4d691ccc597f13408f072c47404ca4f5550232cbe81b0437eea3d7441da" +} diff --git a/lynx/.sqlx/query-fb1228f2d4d9b803809844c0dabaf042551275ffeeccd60485a707f1ddbe9c54.json b/lynx/.sqlx/query-fb1228f2d4d9b803809844c0dabaf042551275ffeeccd60485a707f1ddbe9c54.json new file mode 100644 index 0000000..add53b0 --- /dev/null +++ b/lynx/.sqlx/query-fb1228f2d4d9b803809844c0dabaf042551275ffeeccd60485a707f1ddbe9c54.json @@ -0,0 +1,72 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT id, agent_id, organization_id, user_id,\n command_type, result, error, entry_hash, created_at\n FROM audit_log\n WHERE agent_id = $1\n ORDER BY created_at DESC\n LIMIT $2 OFFSET $3\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "agent_id", + "type_info": "Uuid" + }, + { + "ordinal": 2, + "name": "organization_id", + "type_info": "Uuid" + }, + { + "ordinal": 3, + "name": "user_id", + "type_info": "Uuid" + }, + { + "ordinal": 4, + "name": "command_type", + "type_info": "Text" + }, + { + "ordinal": 5, + "name": "result", + "type_info": "Text" + }, + { + "ordinal": 6, + "name": "error", + "type_info": "Text" + }, + { + "ordinal": 7, + "name": "entry_hash", + "type_info": "Text" + }, + { + "ordinal": 8, + "name": "created_at", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [ + "Uuid", + "Int8", + "Int8" + ] + }, + "nullable": [ + false, + false, + true, + true, + false, + false, + true, + false, + false + ] + }, + "hash": "fb1228f2d4d9b803809844c0dabaf042551275ffeeccd60485a707f1ddbe9c54" +} diff --git a/lynx/.sqlx/query-fb26486d49ed0f35aa74a29a272fda513666210cf3d047d353d33e2a3d7b8a02.json b/lynx/.sqlx/query-fb26486d49ed0f35aa74a29a272fda513666210cf3d047d353d33e2a3d7b8a02.json new file mode 100644 index 0000000..70c23aa --- /dev/null +++ b/lynx/.sqlx/query-fb26486d49ed0f35aa74a29a272fda513666210cf3d047d353d33e2a3d7b8a02.json @@ -0,0 +1,28 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT id, password_hash FROM users WHERE id = $1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "password_hash", + "type_info": "Text" + } + ], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [ + false, + false + ] + }, + "hash": "fb26486d49ed0f35aa74a29a272fda513666210cf3d047d353d33e2a3d7b8a02" +} diff --git a/lynx/.sqlx/query-fb64d4fb0ba9c699f1842b4ba59677e401c7f02c8ee45c1d633752755ec186f4.json b/lynx/.sqlx/query-fb64d4fb0ba9c699f1842b4ba59677e401c7f02c8ee45c1d633752755ec186f4.json new file mode 100644 index 0000000..b619c62 --- /dev/null +++ b/lynx/.sqlx/query-fb64d4fb0ba9c699f1842b4ba59677e401c7f02c8ee45c1d633752755ec186f4.json @@ -0,0 +1,17 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO role_permissions (id, role_id, permission_id, created_by) VALUES ($1, $2, $3, $4)", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid", + "Uuid", + "Uuid", + "Uuid" + ] + }, + "nullable": [] + }, + "hash": "fb64d4fb0ba9c699f1842b4ba59677e401c7f02c8ee45c1d633752755ec186f4" +} diff --git a/lynx/.sqlx/query-fbe8bd62b7adc5ea429279fa71b9448ad856ca88e73223a1b774674cd84900c1.json b/lynx/.sqlx/query-fbe8bd62b7adc5ea429279fa71b9448ad856ca88e73223a1b774674cd84900c1.json new file mode 100644 index 0000000..fa50635 --- /dev/null +++ b/lynx/.sqlx/query-fbe8bd62b7adc5ea429279fa71b9448ad856ca88e73223a1b774674cd84900c1.json @@ -0,0 +1,23 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT COUNT(*) FROM user_roles ur\n JOIN role_permissions rp ON rp.role_id = ur.role_id\n JOIN permissions p ON p.id = rp.permission_id\n WHERE ur.user_id = $1 AND ur.role_id != $2 AND p.key = '*:*'", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "count", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [ + "Uuid", + "Uuid" + ] + }, + "nullable": [ + null + ] + }, + "hash": "fbe8bd62b7adc5ea429279fa71b9448ad856ca88e73223a1b774674cd84900c1" +} diff --git a/lynx/.sqlx/query-fd03da9799102ed1128ada06e403c03de0878482425f648305521ff98555a388.json b/lynx/.sqlx/query-fd03da9799102ed1128ada06e403c03de0878482425f648305521ff98555a388.json new file mode 100644 index 0000000..0344d6e --- /dev/null +++ b/lynx/.sqlx/query-fd03da9799102ed1128ada06e403c03de0878482425f648305521ff98555a388.json @@ -0,0 +1,44 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT id, wg_ip::text AS wg_ip, api_port, status, COALESCE(arch, 'x86_64') AS arch FROM agents WHERE status != 'lockdown'", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "wg_ip", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "api_port", + "type_info": "Int4" + }, + { + "ordinal": 3, + "name": "status", + "type_info": "Text" + }, + { + "ordinal": 4, + "name": "arch", + "type_info": "Text" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + false, + false, + false, + false, + null + ] + }, + "hash": "fd03da9799102ed1128ada06e403c03de0878482425f648305521ff98555a388" +} diff --git a/lynx/.sqlx/query-fdf9e1bc5d284fe3b8e73b2a3320ffd7fa624df6ce92b8ba06fcd520dc6bb995.json b/lynx/.sqlx/query-fdf9e1bc5d284fe3b8e73b2a3320ffd7fa624df6ce92b8ba06fcd520dc6bb995.json new file mode 100644 index 0000000..8793176 --- /dev/null +++ b/lynx/.sqlx/query-fdf9e1bc5d284fe3b8e73b2a3320ffd7fa624df6ce92b8ba06fcd520dc6bb995.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT force_password_change FROM users WHERE id = $1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "force_password_change", + "type_info": "Bool" + } + ], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [ + false + ] + }, + "hash": "fdf9e1bc5d284fe3b8e73b2a3320ffd7fa624df6ce92b8ba06fcd520dc6bb995" +} diff --git a/lynx/.sqlx/query-fe6450e0a791e5e0eff0c3811fdb3f08d549a9bda036725b9ab8dea687e8eaca.json b/lynx/.sqlx/query-fe6450e0a791e5e0eff0c3811fdb3f08d549a9bda036725b9ab8dea687e8eaca.json new file mode 100644 index 0000000..5fa40b1 --- /dev/null +++ b/lynx/.sqlx/query-fe6450e0a791e5e0eff0c3811fdb3f08d549a9bda036725b9ab8dea687e8eaca.json @@ -0,0 +1,14 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO rotation_log (id, triggered_by, reason, scope) VALUES ($1, NULL, 'scheduled', 'all')", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [] + }, + "hash": "fe6450e0a791e5e0eff0c3811fdb3f08d549a9bda036725b9ab8dea687e8eaca" +} diff --git a/lynx/.sqlx/query-feca6f89a63355b01eeafff058ded82493ce8634ab9cb2bf82e3597e349caca2.json b/lynx/.sqlx/query-feca6f89a63355b01eeafff058ded82493ce8634ab9cb2bf82e3597e349caca2.json new file mode 100644 index 0000000..9a655da --- /dev/null +++ b/lynx/.sqlx/query-feca6f89a63355b01eeafff058ded82493ce8634ab9cb2bf82e3597e349caca2.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT id FROM agents WHERE id = $1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + } + ], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [ + false + ] + }, + "hash": "feca6f89a63355b01eeafff058ded82493ce8634ab9cb2bf82e3597e349caca2" +} diff --git a/lynx/Cargo.lock b/lynx/Cargo.lock index ce4975f..3d94be1 100644 --- a/lynx/Cargo.lock +++ b/lynx/Cargo.lock @@ -1823,6 +1823,7 @@ dependencies = [ "tower-http", "tracing", "tracing-subscriber", + "url", "uuid", "zeroize", ] diff --git a/lynx/agent/Cargo.toml b/lynx/agent/Cargo.toml index 36ee8dd..90fa76d 100644 --- a/lynx/agent/Cargo.toml +++ b/lynx/agent/Cargo.toml @@ -39,6 +39,7 @@ tokio-rustls = { workspace = true } # HTTP client (audit log sync to dashboard) reqwest = { version = "0.12", features = ["json", "stream", "rustls-tls"], default-features = false } +url = { workspace = true } # WebSocket client (agent → dashboard persistent connection) tokio-tungstenite = { workspace = true } diff --git a/lynx/agent/migrations/003_nginx_configs.sql b/lynx/agent/migrations/003_nginx_configs.sql index db429fa..672000f 100644 --- a/lynx/agent/migrations/003_nginx_configs.sql +++ b/lynx/agent/migrations/003_nginx_configs.sql @@ -1,5 +1,5 @@ CREATE TABLE nginx_configs ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + id UUID PRIMARY KEY DEFAULT uuidv7(), config_content TEXT NOT NULL, updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); diff --git a/lynx/agent/migrations/005_nftables_output.sql b/lynx/agent/migrations/005_nftables_output.sql new file mode 100644 index 0000000..0a09f96 --- /dev/null +++ b/lynx/agent/migrations/005_nftables_output.sql @@ -0,0 +1,11 @@ +-- Add output chain state entries so agent can persist and restore output rules across reboots. + +-- Extend the check constraint to allow the two output-chain variants. +ALTER TABLE nftables_state DROP CONSTRAINT nftables_state_chain_check; +ALTER TABLE nftables_state ADD CONSTRAINT nftables_state_chain_check + CHECK (chain IN ('lynx-global', 'lynx-local', 'lynx-global-output', 'lynx-local-output')); + +INSERT INTO nftables_state (chain, body, wg_port) VALUES + ('lynx-global-output', '', 51820), + ('lynx-local-output', '', 51820) +ON CONFLICT DO NOTHING; diff --git a/lynx/agent/migrations/006_fix_uuid_default.sql b/lynx/agent/migrations/006_fix_uuid_default.sql new file mode 100644 index 0000000..413325d --- /dev/null +++ b/lynx/agent/migrations/006_fix_uuid_default.sql @@ -0,0 +1,3 @@ +-- Fix UUID column default: gen_random_uuid() generates v4; project requires v7. +-- PostgreSQL 18 provides uuidv7() built-in. +ALTER TABLE nginx_configs ALTER COLUMN id SET DEFAULT uuidv7(); diff --git a/lynx/agent/src/handlers/nftables.rs b/lynx/agent/src/handlers/nftables.rs index 6bc3954..6687afa 100644 --- a/lynx/agent/src/handlers/nftables.rs +++ b/lynx/agent/src/handlers/nftables.rs @@ -12,7 +12,7 @@ pub async fn handle_nftables_apply( )); } - // Chain-specific update: { chain: "lynx-global"|"lynx-local", rules: "..." } + // Chain-specific update if let Some(chain) = cmd.command.get("chain").and_then(|v| v.as_str()) { let rules = cmd .command @@ -24,15 +24,16 @@ pub async fn handle_nftables_apply( match chain { "lynx-global" => state.set_nft_global_body(rules.clone()), "lynx-local" => state.set_nft_local_body(rules.clone()), + "lynx-global-output" => state.set_nft_global_output_body(rules.clone()), + "lynx-local-output" => state.set_nft_local_output_body(rules.clone()), _ => { return Err(AgentError::BadRequest( - "unknown chain: must be lynx-global or lynx-local", + "unknown chain: must be lynx-global, lynx-local, lynx-global-output, or lynx-local-output", )) } } let result = apply_current_ruleset(state)?; - // Persist chain body so the agent can re-apply after reboot. let wg = state.nft_wg_port() as i32; let _ = sqlx::query!( "UPDATE nftables_state SET body = $1, wg_port = $2, updated_at = NOW() WHERE chain = $3", @@ -53,7 +54,6 @@ pub async fn handle_nftables_apply( state.set_nft_wg_port(wg_port); let result = apply_current_ruleset(state)?; - // Persist wg_port for both chains. let wg = wg_port as i32; let _ = sqlx::query!( "UPDATE nftables_state SET wg_port = $1, updated_at = NOW()", @@ -70,10 +70,11 @@ fn apply_current_ruleset(state: &AppState) -> std::result::Result std::result::Result<(), AgentError> { + if domain.is_empty() + || domain.len() > 253 + || domain.contains("..") + || domain.contains('/') + || domain.contains('\0') + || !domain + .chars() + .all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '.') + || domain.starts_with('.') + || domain.ends_with('.') + { + return Err(AgentError::BadRequest("invalid domain for cert path")); + } + Ok(()) +} + /// Close port 19443 via nftables once a domain is confirmed active. pub fn handle_close_setup_port( _state: &AppState, diff --git a/lynx/agent/src/handlers/system.rs b/lynx/agent/src/handlers/system.rs index 6845a2e..f8b3ba3 100644 --- a/lynx/agent/src/handlers/system.rs +++ b/lynx/agent/src/handlers/system.rs @@ -313,17 +313,21 @@ async fn handle_db_rotate_password( )); } - use rand::Rng; - let new_pass: String = rand::thread_rng() - .sample_iter(&rand::distributions::Alphanumeric) - .take(32) - .map(char::from) - .collect(); - - sqlx::query(&format!("ALTER USER lynx_agent_app PASSWORD '{new_pass}'")) - .execute(&state.db) - .await - .map_err(|e| AgentError::Internal(anyhow::anyhow!("ALTER USER: {e}")))?; + use rand::RngCore; + use zeroize::Zeroizing; + let mut buf = [0u8; 24]; + rand::rngs::OsRng.fill_bytes(&mut buf); + let new_pass = Zeroizing::new(buf.iter().map(|b| format!("{b:02x}")).collect::()); + + // Dollar-quoting ($$...$$) avoids any quote-based injection. + // new_pass is hex [0-9a-f] so "$$" can never appear inside it. + sqlx::query(&format!( + "ALTER USER lynx_agent_app PASSWORD $${}$$", + &*new_pass + )) + .execute(&state.db) + .await + .map_err(|e| AgentError::Internal(anyhow::anyhow!("ALTER USER: {e}")))?; let status = std::process::Command::new("podman") .args(["secret", "create", "--replace", "lynx-agent-pg-pass", "-"]) diff --git a/lynx/agent/src/handlers/wireguard.rs b/lynx/agent/src/handlers/wireguard.rs index 39b871f..459def1 100644 --- a/lynx/agent/src/handlers/wireguard.rs +++ b/lynx/agent/src/handlers/wireguard.rs @@ -4,6 +4,7 @@ use crate::{ }; use serde_json::{json, Value}; use std::io::Write; +use zeroize::Zeroizing; use super::containers::require_str; @@ -13,7 +14,7 @@ pub fn handle_wg_rotate_psk(cmd: &VerifiedCommand) -> std::result::Result std::result::Result< } let tunnel_id = require_str(&cmd.command, "tunnel_id")?; - let iface_suffix = tunnel_id.replace('-', ""); - let iface_suffix = &iface_suffix[..iface_suffix.len().min(8)]; + // Strip hyphens and take first 8 chars; then validate only alphanumeric remain. + let iface_suffix_full = tunnel_id.replace('-', ""); + let iface_suffix = &iface_suffix_full[..iface_suffix_full.len().min(8)]; + if !iface_suffix.chars().all(|c| c.is_ascii_alphanumeric()) { + return Err(AgentError::BadRequest( + "tunnel_id produces invalid interface suffix", + )); + } let interface = format!("wg-lynx-dp-{iface_suffix}"); - let local_privkey = require_str(&cmd.command, "private_key")?; + let local_privkey = Zeroizing::new(require_str(&cmd.command, "private_key")?.to_string()); let local_ip_cidr = require_str(&cmd.command, "local_ip")?; let peer_pubkey = require_str(&cmd.command, "peer_pubkey")?; - let psk = require_str(&cmd.command, "psk")?; + let psk = Zeroizing::new(require_str(&cmd.command, "psk")?.to_string()); let wg_port = cmd .command .get("wg_port") @@ -123,14 +130,25 @@ pub fn handle_wg_data_plane_setup(cmd: &VerifiedCommand) -> std::result::Result< .map(|ep| format!("Endpoint = {ep}\n")) .unwrap_or_default(); - let config = format!( - "[Interface]\nPrivateKey = {local_privkey}\nAddress = {local_ip_cidr}\nListenPort = {wg_port}\n\n[Peer]\nPublicKey = {peer_pubkey}\nPresharedKey = {psk}\nAllowedIPs = {peer_allowed}\n{endpoint_line}" - ); - - let mut f = std::fs::File::create(&config_path) - .map_err(|e| AgentError::Internal(anyhow::anyhow!("write wg config {config_path}: {e}")))?; - f.write_all(config.as_bytes()) - .map_err(|e| AgentError::Internal(anyhow::anyhow!("write wg config content: {e}")))?; + let config = Zeroizing::new(format!( + "[Interface]\nPrivateKey = {}\nAddress = {local_ip_cidr}\nListenPort = {wg_port}\n\n[Peer]\nPublicKey = {peer_pubkey}\nPresharedKey = {}\nAllowedIPs = {peer_allowed}\n{endpoint_line}", + &*local_privkey, &*psk + )); + + { + use std::os::unix::fs::OpenOptionsExt; + let mut f = std::fs::OpenOptions::new() + .write(true) + .create(true) + .truncate(true) + .mode(0o600) + .open(&config_path) + .map_err(|e| { + AgentError::Internal(anyhow::anyhow!("write wg config {config_path}: {e}")) + })?; + f.write_all(config.as_bytes()) + .map_err(|e| AgentError::Internal(anyhow::anyhow!("write wg config content: {e}")))?; + } let status = std::process::Command::new("wg-quick") .args(["up", &interface]) @@ -163,8 +181,13 @@ pub fn handle_wg_data_plane_teardown( } let tunnel_id = require_str(&cmd.command, "tunnel_id")?; - let iface_suffix = tunnel_id.replace('-', ""); - let iface_suffix = &iface_suffix[..iface_suffix.len().min(8)]; + let iface_suffix_full = tunnel_id.replace('-', ""); + let iface_suffix = &iface_suffix_full[..iface_suffix_full.len().min(8)]; + if !iface_suffix.chars().all(|c| c.is_ascii_alphanumeric()) { + return Err(AgentError::BadRequest( + "tunnel_id produces invalid interface suffix", + )); + } let interface = format!("wg-lynx-dp-{iface_suffix}"); let config_path = format!("/etc/wireguard/{interface}.conf"); diff --git a/lynx/agent/src/main.rs b/lynx/agent/src/main.rs index 0123f02..c1bd54a 100644 --- a/lynx/agent/src/main.rs +++ b/lynx/agent/src/main.rs @@ -11,13 +11,13 @@ mod nginx; mod podman; mod state; mod sync; -mod update; +pub mod update; mod ws_client; use anyhow::Context; use axum::{ extract::State, - http::{HeaderMap, StatusCode}, + http::StatusCode, response::{IntoResponse, Response}, routing::{get, post}, Json, Router, @@ -184,10 +184,13 @@ async fn main() -> anyhow::Result<()> { nft_last_ruleset: Arc::new(std::sync::Mutex::new(None)), nft_global_body: Arc::new(std::sync::Mutex::new(String::new())), nft_local_body: Arc::new(std::sync::Mutex::new(String::new())), + nft_global_output_body: Arc::new(std::sync::Mutex::new(String::new())), + nft_local_output_body: Arc::new(std::sync::Mutex::new(String::new())), nft_wg_port: Arc::new(std::sync::atomic::AtomicU32::new(51820)), cmd_rate: Arc::new(std::sync::Mutex::new((0u64, 0u64))), cmd_rejected_count: Arc::new(std::sync::atomic::AtomicU64::new(0)), cmd_rejected_window: Arc::new(std::sync::atomic::AtomicU64::new(0)), + last_dashboard_contact: Arc::new(std::sync::atomic::AtomicU64::new(0)), }; // Reload nftables state from DB and re-apply on startup (rules don't persist across reboots). @@ -199,12 +202,16 @@ async fn main() -> anyhow::Result<()> { if let Ok(rows) = rows { let mut global_body = String::new(); let mut local_body = String::new(); + let mut global_output_body = String::new(); + let mut local_output_body = String::new(); let mut wg_port = 51820u16; for row in &rows { match row.chain.as_str() { "lynx-global" => global_body = row.body.clone(), "lynx-local" => local_body = row.body.clone(), + "lynx-global-output" => global_output_body = row.body.clone(), + "lynx-local-output" => local_output_body = row.body.clone(), _ => {} } wg_port = row.wg_port as u16; @@ -212,6 +219,8 @@ async fn main() -> anyhow::Result<()> { state.set_nft_global_body(global_body); state.set_nft_local_body(local_body); + state.set_nft_global_output_body(global_output_body); + state.set_nft_local_output_body(local_output_body); state.set_nft_wg_port(wg_port); let ruleset = nftables::Ruleset { @@ -219,6 +228,8 @@ async fn main() -> anyhow::Result<()> { org_networks: vec![], global_body: state.nft_global_body(), local_body: state.nft_local_body(), + global_output_body: state.nft_global_output_body(), + local_output_body: state.nft_local_output_body(), }; match nftables::apply(&ruleset) { @@ -261,6 +272,9 @@ async fn main() -> anyhow::Result<()> { // WebSocket client — persistent connection to dashboard tokio::spawn(ws_client::run_ws_client(state.clone())); + // Fallback self-updater: polls GitHub directly if dashboard absent for >6h + tokio::spawn(update::fallback::run_fallback_updater(state.clone())); + // Audit log sync task (HTTP batch fallback when WS is down) tokio::spawn(sync::run_sync_task(state.clone())); @@ -291,19 +305,13 @@ async fn main() -> anyhow::Result<()> { // Build TLS acceptor before moving state into router. let tls_acceptor = build_tls_acceptor(&state.config); - // Pass last_heartbeat to the heartbeat route via extension - let hb = last_heartbeat.clone(); let app = Router::new() .route("/health", get(handlers::health)) .route("/cmd", post(handlers::execute_command)) .route("/metrics/ws", get(handlers::metrics_ws)) - .route( - "/heartbeat", - post(move |State(state): State, headers: HeaderMap| { - let hb = hb.clone(); - async move { heartbeat_handler(state, headers, hb).await } - }), - ) + .route("/heartbeat", post(heartbeat_handler)) + // Inject last_heartbeat as a layer extension so the handler can access it. + .layer(axum::Extension(last_heartbeat.clone())) .with_state(state); let listener = tokio::net::TcpListener::bind(&listen_addr).await?; @@ -323,18 +331,36 @@ async fn main() -> anyhow::Result<()> { } async fn heartbeat_handler( - state: AppState, - headers: HeaderMap, - hb: Arc>, + State(state): State, + hb: axum::Extension>>, + Json(signed): Json, ) -> Response { - let token = headers - .get(axum::http::header::AUTHORIZATION) - .and_then(|v| v.to_str().ok()) - .and_then(|v| v.strip_prefix("Bearer ")) - .unwrap_or(""); + // Heartbeat ACK requires a valid Ed25519 signature — bearer token alone is + // insufficient so that `internal_token` compromise cannot suppress lockdown. + let verified = auth::verify_command( + &state.db, + &signed, + &state.config.dashboard_verify_key, + state.config.agent_id, + ) + .await; + + let cmd = match verified { + Ok(c) => c, + Err(e) => { + tracing::warn!("heartbeat ACK rejected: invalid signature: {e}"); + return StatusCode::UNAUTHORIZED.into_response(); + } + }; - if !auth::verify_bearer(token, &state.config.internal_token) { - return StatusCode::UNAUTHORIZED.into_response(); + let cmd_type = cmd + .command + .get("type") + .and_then(|v| v.as_str()) + .unwrap_or(""); + if cmd_type != "agent.heartbeat_ack" { + tracing::warn!("heartbeat endpoint received unexpected command type: {cmd_type}"); + return StatusCode::BAD_REQUEST.into_response(); } *hb.lock().unwrap() = std::time::Instant::now(); diff --git a/lynx/agent/src/nftables/mod.rs b/lynx/agent/src/nftables/mod.rs index 6e5f855..a76ab5c 100644 --- a/lynx/agent/src/nftables/mod.rs +++ b/lynx/agent/src/nftables/mod.rs @@ -8,16 +8,21 @@ const TABLE: &str = "lynx-agent"; /// Full structure of the managed ruleset. /// lynx-base holds the immutable invariants. -/// lynx-global / lynx-local hold dashboard-pushed rules. +/// lynx-global / lynx-local hold dashboard-pushed input rules. +/// lynx-global-output / lynx-local-output hold dashboard-pushed output rules. pub struct Ruleset { /// WireGuard UDP port for management plane pub wireguard_port: u16, /// Per-org blocked subnets (org isolation — inter-org traffic blocked) pub org_networks: Vec, - /// Rules body for the lynx-global chain (dashboard-pushed, applies to all agents) + /// Input rules body for the lynx-global chain (dashboard-pushed, applies to all agents) pub global_body: String, - /// Rules body for the lynx-local chain (dashboard-pushed, this agent only) + /// Input rules body for the lynx-local chain (dashboard-pushed, this agent only) pub local_body: String, + /// Output rules body for the lynx-global-output chain (dashboard-pushed, applies to all agents) + pub global_output_body: String, + /// Output rules body for the lynx-local-output chain (dashboard-pushed, this agent only) + pub local_output_body: String, } pub struct OrgNetwork { @@ -116,12 +121,12 @@ table inet {TABLE} {{ drop }} - # Dashboard global rules — apply to all agents + # Dashboard global rules — input, apply to all agents chain lynx-global {{ {global} }} - # Dashboard local rules — apply to this agent only + # Dashboard local rules — input, apply to this agent only chain lynx-local {{ {local} }} @@ -143,9 +148,24 @@ table inet {TABLE} {{ )); } - out.push_str( - " }\n\n chain lynx-output {\n type filter hook output priority 0; policy accept;\n }\n}\n", - ); + out.push_str(&format!( + r#" }} + + chain lynx-output {{ + type filter hook output priority 0; policy accept; + + # Dashboard global output rules — apply to all agents +{global_out} + + # Dashboard local output rules — apply to this agent only +{local_out} + }} +}} +"#, + global_out = r.global_output_body, + local_out = r.local_output_body, + )); + out } diff --git a/lynx/agent/src/podman/mod.rs b/lynx/agent/src/podman/mod.rs index 9fe60ea..30f9975 100644 --- a/lynx/agent/src/podman/mod.rs +++ b/lynx/agent/src/podman/mod.rs @@ -44,11 +44,18 @@ pub fn ensure_tenant_user(tenant_id: &str) -> Result<()> { } /// Run a Podman command as a specific tenant user via `runuser`. +/// +/// Uses `-u` (not `-l -c`) so each argument is passed directly to the OS without +/// shell interpretation — prevents command injection through container/image names. +/// Sets HOME and XDG_RUNTIME_DIR so rootless Podman finds its storage and socket. pub fn podman_as_tenant(tenant_id: &str, args: &[&str]) -> Result { let username = format!("lynx-tenant-{tenant_id}"); + let uid = tenant_uid(tenant_id)?; Command::new("runuser") - .args(["-l", &username, "-c"]) - .arg(format!("podman {}", args.join(" "))) + .args(["-u", &username, "--", "podman"]) + .args(args) + .env("HOME", format!("/var/lib/lynx/orgs/{tenant_id}")) + .env("XDG_RUNTIME_DIR", format!("/run/user/{uid}")) .output() .context("runuser podman") } diff --git a/lynx/agent/src/state.rs b/lynx/agent/src/state.rs index ab2b7f0..8043898 100644 --- a/lynx/agent/src/state.rs +++ b/lynx/agent/src/state.rs @@ -15,10 +15,14 @@ pub struct AppState { pub nft_checksum: Arc>>, /// Rendered nft ruleset from last successful apply() — used for restore. pub nft_last_ruleset: Arc>>, - /// Body of the lynx-global chain (managed by dashboard global rules). + /// Body of the lynx-global chain (input, managed by dashboard global rules). pub nft_global_body: Arc>, - /// Body of the lynx-local chain (managed by dashboard local rules for this agent). + /// Body of the lynx-local chain (input, managed by dashboard local rules for this agent). pub nft_local_body: Arc>, + /// Body of the lynx-global-output chain (output, managed by dashboard global rules). + pub nft_global_output_body: Arc>, + /// Body of the lynx-local-output chain (output, managed by dashboard local rules for this agent). + pub nft_local_output_body: Arc>, /// WireGuard port used in the last full nftables apply (stored for chain-only updates). pub nft_wg_port: Arc, /// In-memory command rate limiter: (window_start_secs, count_in_window) @@ -27,6 +31,9 @@ pub struct AppState { pub cmd_rejected_count: Arc, /// Epoch-second when the current rejection-count minute window started. pub cmd_rejected_window: Arc, + /// Epoch-second of last successful dashboard contact (WS connect or message received). + /// 0 = never connected. Used by the fallback updater to detect dashboard absence. + pub last_dashboard_contact: Arc, } impl AppState { @@ -108,4 +115,20 @@ impl AppState { pub fn nft_local_body(&self) -> String { self.nft_local_body.lock().unwrap().clone() } + + pub fn set_nft_global_output_body(&self, body: String) { + *self.nft_global_output_body.lock().unwrap() = body; + } + + pub fn nft_global_output_body(&self) -> String { + self.nft_global_output_body.lock().unwrap().clone() + } + + pub fn set_nft_local_output_body(&self, body: String) { + *self.nft_local_output_body.lock().unwrap() = body; + } + + pub fn nft_local_output_body(&self) -> String { + self.nft_local_output_body.lock().unwrap().clone() + } } diff --git a/lynx/agent/src/update/fallback.rs b/lynx/agent/src/update/fallback.rs new file mode 100644 index 0000000..3590e8a --- /dev/null +++ b/lynx/agent/src/update/fallback.rs @@ -0,0 +1,128 @@ +use crate::state::AppState; +use anyhow::Context as _; +use std::sync::atomic::Ordering; +use tokio::time::{interval, Duration}; + +/// How long the dashboard must be unreachable before the agent polls GitHub directly. +const ABSENT_THRESHOLD_SECS: u64 = 6 * 3600; + +/// How often the fallback updater checks if an update is needed. +const CHECK_INTERVAL_SECS: u64 = 3600; + +const GITHUB_API: &str = "https://api.github.com/repos/Jaro-c/Lynx/releases"; + +pub async fn run_fallback_updater(state: AppState) { + let mut ticker = interval(Duration::from_secs(CHECK_INTERVAL_SECS)); + ticker.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip); + + loop { + ticker.tick().await; + + let last_contact = state.last_dashboard_contact.load(Ordering::SeqCst); + let now = epoch_secs(); + + // 0 = never connected. If we have never connected, use a past epoch so the absence + // threshold is immediately satisfied — agent might have been offline since install. + let absent_secs = if last_contact == 0 { + ABSENT_THRESHOLD_SECS + 1 + } else { + now.saturating_sub(last_contact) + }; + + if absent_secs <= ABSENT_THRESHOLD_SECS { + continue; + } + + tracing::info!( + absent_secs, + "dashboard absent — checking GitHub for agent update" + ); + + if let Err(e) = check_and_apply(&state).await { + tracing::warn!(error = %e, "fallback updater check failed"); + } + } +} + +async fn check_and_apply(state: &AppState) -> anyhow::Result<()> { + let current_version = &state.config.version; + let arch = match std::env::consts::ARCH { + "aarch64" => "arm64", + a => a, + }; + + let latest = fetch_latest_agent_version().await?; + + if !is_newer(&latest, current_version) { + tracing::debug!(current = current_version, latest, "agent is up to date"); + return Ok(()); + } + + tracing::info!( + current = current_version, + latest, + "fallback: applying agent update" + ); + + let download_url = format!( + "https://github.com/Jaro-c/Lynx/releases/download/agent@{latest}/lynx-agent-linux-{arch}" + ); + let sig_url = format!("{download_url}.sig"); + + super::perform_update(&latest, &download_url, &sig_url).await +} + +async fn fetch_latest_agent_version() -> anyhow::Result { + let client = super::build_ssrf_safe_client(GITHUB_API) + .await + .context("SSRF check for GitHub API")?; + + let releases: serde_json::Value = client + .get(GITHUB_API) + .send() + .await? + .error_for_status()? + .json() + .await?; + + let releases = releases + .as_array() + .ok_or_else(|| anyhow::anyhow!("GitHub API returned non-array"))?; + + for release in releases { + let tag = release + .get("tag_name") + .and_then(|v| v.as_str()) + .unwrap_or(""); + if let Some(ver) = tag.strip_prefix("agent@") { + return Ok(ver.to_string()); + } + } + + anyhow::bail!("no agent@* release found in GitHub releases") +} + +/// Returns true if `latest` is strictly newer than `current` (semver comparison). +fn is_newer(latest: &str, current: &str) -> bool { + parse_semver(latest) > parse_semver(current) +} + +fn parse_semver(v: &str) -> (u64, u64, u64) { + let parts: Vec = v + .trim_start_matches('v') + .splitn(3, '.') + .map(|s| s.parse().unwrap_or(0)) + .collect(); + ( + parts.first().copied().unwrap_or(0), + parts.get(1).copied().unwrap_or(0), + parts.get(2).copied().unwrap_or(0), + ) +} + +fn epoch_secs() -> u64 { + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap_or_default() + .as_secs() +} diff --git a/lynx/agent/src/update/mod.rs b/lynx/agent/src/update/mod.rs index 5ab3411..d43c5f3 100644 --- a/lynx/agent/src/update/mod.rs +++ b/lynx/agent/src/update/mod.rs @@ -1,28 +1,37 @@ +pub mod fallback; + use anyhow::{Context, Result}; use ed25519_dalek::{Signature, Verifier, VerifyingKey}; use std::path::{Path, PathBuf}; /// Download new binary, verify Ed25519 signature, atomic swap, then exec into new process. /// -/// The public key used for signature verification is the same Ed25519 key -/// the dashboard uses to sign commands (DASHBOARD_VERIFY_KEY env var). -/// Release binaries are signed with the corresponding private key at release time. +/// The release verify key (`RELEASE_VERIFY_KEY_B64`) is compiled into the binary and is distinct +/// from the dashboard command-signing key. The corresponding private key lives only in GitHub +/// Actions secrets — compromising the repo or the dashboard does not allow forging signatures. pub async fn perform_update(version: &str, download_url: &str, sig_url: &str) -> Result<()> { + validate_github_url(download_url)?; + validate_github_url(sig_url)?; + tracing::info!(version, "starting self-update"); - let client = reqwest::Client::builder() - .user_agent(format!("lynx-agent/{}", env!("CARGO_PKG_VERSION"))) - .timeout(std::time::Duration::from_secs(300)) - .build() - .context("build HTTP client")?; + // Build separate SSRF-safe clients per URL: resolves DNS once, validates + // the resolved IP is not RFC1918/loopback, then pins the hostname to that + // IP for the actual request (prevents DNS TOCTOU rebinding attacks). + let bin_client = build_ssrf_safe_client(download_url) + .await + .context("SSRF check for binary URL")?; + let sig_client = build_ssrf_safe_client(sig_url) + .await + .context("SSRF check for sig URL")?; // Download binary - let binary_bytes = download_bytes(&client, download_url) + let binary_bytes = download_bytes(&bin_client, download_url) .await .context("download binary")?; // Download signature - let sig_bytes = download_bytes(&client, sig_url) + let sig_bytes = download_bytes(&sig_client, sig_url) .await .context("download signature")?; @@ -96,21 +105,15 @@ fn verify_signature(binary: &[u8], sig_bytes: &[u8]) -> Result<()> { .context("Ed25519 signature invalid") } +const RELEASE_VERIFY_KEY_B64: &str = "OsBV4t+vQSn10FAI8UzAJEBS0IUqp8D2bZtlQYD8j+Q="; + fn load_verify_key() -> Result<[u8; 32]> { use base64ct::{Base64, Encoding}; - - // Reuse DASHBOARD_VERIFY_KEY env / file — same key signs commands and release binaries. - let raw = if let Ok(path) = std::env::var("DASHBOARD_VERIFY_KEY_FILE") { - std::fs::read_to_string(&path) - .with_context(|| format!("read DASHBOARD_VERIFY_KEY_FILE={path}"))? - } else { - std::env::var("DASHBOARD_VERIFY_KEY").context("DASHBOARD_VERIFY_KEY not configured")? - }; - - let bytes = Base64::decode_vec(raw.trim()).context("base64 decode DASHBOARD_VERIFY_KEY")?; + let bytes = Base64::decode_vec(RELEASE_VERIFY_KEY_B64) + .context("decode hardcoded release verify key")?; bytes .try_into() - .map_err(|_| anyhow::anyhow!("DASHBOARD_VERIFY_KEY must be 32 bytes")) + .map_err(|_| anyhow::anyhow!("release verify key must be 32 bytes")) } fn tmp_path(exe: &Path) -> PathBuf { @@ -122,3 +125,70 @@ fn tmp_path(exe: &Path) -> PathBuf { p.set_file_name(format!("{name}.new")); p } + +fn validate_github_url(url: &str) -> Result<()> { + let allowed = [ + "https://github.com/", + "https://objects.githubusercontent.com/", + ]; + if allowed.iter().any(|prefix| url.starts_with(prefix)) { + Ok(()) + } else { + anyhow::bail!("download URL not on allowed domain: {url}") + } +} + +/// Builds an HTTP client with SSRF protection: +/// 1. Resolves the hostname of `url` via DNS (once). +/// 2. Rejects if any resolved IP is RFC1918, loopback, or link-local. +/// 3. Pins the hostname to the validated IP so reqwest never re-resolves it +/// (prevents DNS rebinding / TOCTOU attacks). +async fn build_ssrf_safe_client(url: &str) -> Result { + let parsed = url::Url::parse(url).context("parse URL for SSRF check")?; + let host = parsed + .host_str() + .ok_or_else(|| anyhow::anyhow!("URL has no host: {url}"))? + .to_string(); + let port = parsed + .port_or_known_default() + .ok_or_else(|| anyhow::anyhow!("URL has unknown port: {url}"))?; + + let addrs: Vec = tokio::net::lookup_host(format!("{host}:{port}")) + .await + .with_context(|| format!("DNS lookup for {host}"))? + .collect(); + + if addrs.is_empty() { + anyhow::bail!("DNS lookup for {host} returned no addresses"); + } + + for addr in &addrs { + if is_private_ip(addr.ip()) { + anyhow::bail!( + "SSRF protection: {host} resolved to private/reserved IP {}", + addr.ip() + ); + } + } + + reqwest::Client::builder() + .user_agent(format!("lynx-agent/{}", env!("CARGO_PKG_VERSION"))) + .timeout(std::time::Duration::from_secs(300)) + .resolve(&host, addrs[0]) + .build() + .context("build SSRF-safe HTTP client") +} + +fn is_private_ip(ip: std::net::IpAddr) -> bool { + match ip { + std::net::IpAddr::V4(v4) => { + v4.is_private() || v4.is_loopback() || v4.is_link_local() || v4.is_unspecified() + } + std::net::IpAddr::V6(v6) => { + v6.is_loopback() + || v6.is_unspecified() + || (v6.segments()[0] & 0xfe00) == 0xfc00 // fc00::/7 ULA + || (v6.segments()[0] & 0xffc0) == 0xfe80 // fe80::/10 link-local + } + } +} diff --git a/lynx/agent/src/ws_client.rs b/lynx/agent/src/ws_client.rs index 0be452b..7207d27 100644 --- a/lynx/agent/src/ws_client.rs +++ b/lynx/agent/src/ws_client.rs @@ -49,6 +49,7 @@ pub async fn run_ws_client(state: AppState) { Ok((ws_stream, _)) => { backoff = BACKOFF_BASE; tracing::info!("dashboard WS connected"); + record_dashboard_contact(&state); run_session(&state, ws_stream).await; tracing::warn!("dashboard WS session ended — reconnecting"); } @@ -115,6 +116,7 @@ async fn run_session( #[allow(clippy::collapsible_match)] match msg { Some(Ok(Message::Text(text))) => { + record_dashboard_contact(state); let reply = handle_message(state, text.as_str()).await; if let Some(frame) = reply { let text = serde_json::to_string(&frame).unwrap_or_default(); @@ -138,11 +140,26 @@ async fn run_session( } } +fn record_dashboard_contact(state: &AppState) { + let now = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap_or_default() + .as_secs(); + state + .last_dashboard_contact + .store(now, std::sync::atomic::Ordering::SeqCst); +} + fn heartbeat_payload(state: &AppState) -> Value { + let arch = match std::env::consts::ARCH { + "aarch64" => "arm64", + a => a, + }; json!({ "type": "heartbeat", "agent_id": state.config.agent_id, "version": state.config.version, + "arch": arch, "timestamp": chrono::Utc::now().to_rfc3339(), "status": if state.lockdown.load(Ordering::SeqCst) { "lockdown" } else { "online" }, "nonce": Uuid::now_v7(), diff --git a/lynx/dashboard/.env.example b/lynx/dashboard/.env.example index 09d8c24..6aebf1f 100644 --- a/lynx/dashboard/.env.example +++ b/lynx/dashboard/.env.example @@ -3,19 +3,18 @@ POSTGRES_USER=lynx POSTGRES_DB=lynx_dashboard RUST_LOG=info -# --- Dev-only env vars (never commit the real .env) ------------------------- -# Copy to .env and fill in values for local development. -# In production these are Podman secrets mounted at /run/secrets/ via *_FILE vars. - -# DATABASE_URL=postgresql://lynx_dashboard_app:@localhost:5433/lynx_dashboard -# REDIS_URL=redis://:@localhost:6379 -# INTERNAL_API_TOKEN= # openssl rand -hex 32 -# KEK= # openssl rand -base64 32 | tr -d '\n' -# PEPPER= # openssl rand -hex 32 +# Secrets — in production these are Podman secrets mounted at /run/secrets/ +# via *_FILE env vars. Never commit real values. +# +# DATABASE_URL=postgresql://lynx_dashboard_app:@postgres/lynx_dashboard +# REDIS_URL=redis://:@redis:6379 +# INTERNAL_API_TOKEN= # openssl rand -hex 32 +# KEK= # openssl rand -base64 32 | tr -d '\n' +# PEPPER= # openssl rand -hex 32 # JWT_SIGN_PRIVATE_KEY= # JWT_SIGN_PUBLIC_KEY= # JWT_ENC_PRIVATE_KEY= # JWT_ENC_PUBLIC_KEY= # CA_PRIVATE_KEY= # CA_PUBLIC_KEY= -# SETUP_TOKEN= # openssl rand -hex 32 — one-time bootstrap token +# SETUP_TOKEN= # openssl rand -hex 32 — one-time bootstrap token diff --git a/lynx/dashboard/docker-compose.dev.yml b/lynx/dashboard/docker-compose.dev.yml deleted file mode 100644 index 14cef6f..0000000 --- a/lynx/dashboard/docker-compose.dev.yml +++ /dev/null @@ -1,30 +0,0 @@ -services: - postgres: - image: docker.io/library/postgres@sha256:bfae840554bdbd4e9f8d097d8e23ffda8aac82866e04ea0d6bc09647234dd359 - environment: - - POSTGRES_USER=lynx - - POSTGRES_DB=lynx_dashboard - - POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-lynx_dev} - ports: - - "5433:5432" - volumes: - - postgres_dev_data:/var/lib/postgresql/data - healthcheck: - test: ["CMD-SHELL", "pg_isready -U lynx -d lynx_dashboard"] - interval: 5s - timeout: 3s - retries: 10 - - redis: - image: docker.io/library/redis@sha256:02c2454cd3b6277389101b9d42d009f8518255930f8b06ae33624e324f8c6455 - command: redis-server --save "" --appendonly no - ports: - - "6379:6379" - healthcheck: - test: ["CMD", "redis-cli", "ping"] - interval: 5s - timeout: 3s - retries: 10 - -volumes: - postgres_dev_data: diff --git a/lynx/dashboard/docker-compose.yml b/lynx/dashboard/docker-compose.yml index c5ed49d..a0cc365 100644 --- a/lynx/dashboard/docker-compose.yml +++ b/lynx/dashboard/docker-compose.yml @@ -1,27 +1,40 @@ services: frontend: container_name: lynx-dashboard-frontend - build: - context: ./ui - dockerfile: Dockerfile - target: runner + image: docker.io/library/alpine@sha256:48b0309ca019d89d40f670aa1bc06e426dc0931948452e8491e3d65087abc07d + working_dir: /etc/lynx/frontend + command: ["/etc/lynx/frontend/lynx-dashboard-frontend"] ports: - "19443:3000" environment: - NODE_ENV=production + - PORT=3000 + - HOSTNAME=0.0.0.0 - BACKEND_URL=http://lynx-dashboard-backend:8080 + - NEXT_TELEMETRY_DISABLED=1 + volumes: + - /etc/lynx/frontend:/etc/lynx/frontend:ro depends_on: backend: condition: service_healthy + healthcheck: + test: ["CMD", "wget", "-qO", "/dev/null", "http://localhost:3000"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 20s restart: unless-stopped networks: - lynx-dashboard-app backend: container_name: lynx-dashboard-backend - build: - context: ./server - dockerfile: Dockerfile + image: docker.io/library/alpine@sha256:48b0309ca019d89d40f670aa1bc06e426dc0931948452e8491e3d65087abc07d + command: ["/etc/lynx/bin/lynx-dashboard-backend"] + volumes: + - /etc/lynx/bin:/etc/lynx/bin + - /etc/lynx/frontend:/etc/lynx/frontend + - /run/podman/podman.sock:/run/podman/podman.sock environment: - DATABASE_URL_FILE=/run/secrets/lynx-dashboard-database-url - REDIS_URL_FILE=/run/secrets/lynx-dashboard-redis-url @@ -55,7 +68,7 @@ services: redis: condition: service_healthy healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:8080/health"] + test: ["CMD", "wget", "-qO", "/dev/null", "http://localhost:8080/health"] interval: 10s timeout: 5s retries: 5 diff --git a/lynx/dashboard/server/.sqlx/query-01bfbb58c095c306159c29c82daf5a72a9c1cf292d72b84f4a78bd1c18839b79.json b/lynx/dashboard/server/.sqlx/query-01bfbb58c095c306159c29c82daf5a72a9c1cf292d72b84f4a78bd1c18839b79.json new file mode 100644 index 0000000..fd496a9 --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-01bfbb58c095c306159c29c82daf5a72a9c1cf292d72b84f4a78bd1c18839b79.json @@ -0,0 +1,17 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO user_roles (id, user_id, role_id, created_by) VALUES ($1, $2, $3, $4)", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid", + "Uuid", + "Uuid", + "Uuid" + ] + }, + "nullable": [] + }, + "hash": "01bfbb58c095c306159c29c82daf5a72a9c1cf292d72b84f4a78bd1c18839b79" +} diff --git a/lynx/dashboard/server/.sqlx/query-039956bbaa6983ec2bc328db9bc1c0c9b088c6e612d88bae53f9e1466b8b48f3.json b/lynx/dashboard/server/.sqlx/query-039956bbaa6983ec2bc328db9bc1c0c9b088c6e612d88bae53f9e1466b8b48f3.json new file mode 100644 index 0000000..1c3a455 --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-039956bbaa6983ec2bc328db9bc1c0c9b088c6e612d88bae53f9e1466b8b48f3.json @@ -0,0 +1,52 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT o.id, o.name, o.slug, o.owner_id, o.created_at,\n COUNT(m.user_id) AS \"member_count!\"\n FROM organizations o\n JOIN organization_members m ON m.organization_id = o.id\n WHERE m.user_id = $1\n GROUP BY o.id\n ORDER BY o.created_at ASC\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "name", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "slug", + "type_info": "Text" + }, + { + "ordinal": 3, + "name": "owner_id", + "type_info": "Uuid" + }, + { + "ordinal": 4, + "name": "created_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 5, + "name": "member_count!", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [ + false, + false, + false, + false, + false, + null + ] + }, + "hash": "039956bbaa6983ec2bc328db9bc1c0c9b088c6e612d88bae53f9e1466b8b48f3" +} diff --git a/lynx/dashboard/server/.sqlx/query-04033eaad30ce63f08958fabfb5604aab131889512feeef9e61d33e07051ce42.json b/lynx/dashboard/server/.sqlx/query-04033eaad30ce63f08958fabfb5604aab131889512feeef9e61d33e07051ce42.json new file mode 100644 index 0000000..09bf9c4 --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-04033eaad30ce63f08958fabfb5604aab131889512feeef9e61d33e07051ce42.json @@ -0,0 +1,56 @@ +{ + "db_name": "PostgreSQL", + "query": "\n INSERT INTO projects (id, organization_id, agent_id, name, slug)\n VALUES ($1, $2, $3, $4, $5)\n RETURNING id, organization_id, agent_id, name, slug, created_at\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "organization_id", + "type_info": "Uuid" + }, + { + "ordinal": 2, + "name": "agent_id", + "type_info": "Uuid" + }, + { + "ordinal": 3, + "name": "name", + "type_info": "Text" + }, + { + "ordinal": 4, + "name": "slug", + "type_info": "Text" + }, + { + "ordinal": 5, + "name": "created_at", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [ + "Uuid", + "Uuid", + "Uuid", + "Text", + "Text" + ] + }, + "nullable": [ + false, + false, + false, + false, + false, + false + ] + }, + "hash": "04033eaad30ce63f08958fabfb5604aab131889512feeef9e61d33e07051ce42" +} diff --git a/lynx/dashboard/server/.sqlx/query-04fa60cecac1c0c581ddd58cd4092413a6f8a3306c1a9e28921d410f35c243cc.json b/lynx/dashboard/server/.sqlx/query-04fa60cecac1c0c581ddd58cd4092413a6f8a3306c1a9e28921d410f35c243cc.json new file mode 100644 index 0000000..77c0b0e --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-04fa60cecac1c0c581ddd58cd4092413a6f8a3306c1a9e28921d410f35c243cc.json @@ -0,0 +1,23 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT agent_id FROM projects WHERE id = $1 AND organization_id = $2", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "agent_id", + "type_info": "Uuid" + } + ], + "parameters": { + "Left": [ + "Uuid", + "Uuid" + ] + }, + "nullable": [ + false + ] + }, + "hash": "04fa60cecac1c0c581ddd58cd4092413a6f8a3306c1a9e28921d410f35c243cc" +} diff --git a/lynx/dashboard/server/.sqlx/query-05755ff8b74059273802f4586f7f8b640c6340e55407e4a15cce8d744557e7ad.json b/lynx/dashboard/server/.sqlx/query-05755ff8b74059273802f4586f7f8b640c6340e55407e4a15cce8d744557e7ad.json new file mode 100644 index 0000000..8de728b --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-05755ff8b74059273802f4586f7f8b640c6340e55407e4a15cce8d744557e7ad.json @@ -0,0 +1,15 @@ +{ + "db_name": "PostgreSQL", + "query": "DELETE FROM role_permissions WHERE role_id = $1 AND permission_id = $2", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid", + "Uuid" + ] + }, + "nullable": [] + }, + "hash": "05755ff8b74059273802f4586f7f8b640c6340e55407e4a15cce8d744557e7ad" +} diff --git a/lynx/dashboard/server/.sqlx/query-07a0f39a809ce15570868cd4f1b02346ce233a4a6468b97c016bd52f4da349cb.json b/lynx/dashboard/server/.sqlx/query-07a0f39a809ce15570868cd4f1b02346ce233a4a6468b97c016bd52f4da349cb.json new file mode 100644 index 0000000..32a1415 --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-07a0f39a809ce15570868cd4f1b02346ce233a4a6468b97c016bd52f4da349cb.json @@ -0,0 +1,34 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT wg_ip, api_port, status FROM agents WHERE id = $1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "wg_ip", + "type_info": "Text" + }, + { + "ordinal": 1, + "name": "api_port", + "type_info": "Int4" + }, + { + "ordinal": 2, + "name": "status", + "type_info": "Text" + } + ], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [ + false, + false, + false + ] + }, + "hash": "07a0f39a809ce15570868cd4f1b02346ce233a4a6468b97c016bd52f4da349cb" +} diff --git a/lynx/dashboard/server/.sqlx/query-07bfb9d7ecdb22c8e2e0d94b66f3ec87a4499b4895a586ae8cb4f70d2d23b216.json b/lynx/dashboard/server/.sqlx/query-07bfb9d7ecdb22c8e2e0d94b66f3ec87a4499b4895a586ae8cb4f70d2d23b216.json new file mode 100644 index 0000000..c0d30dc --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-07bfb9d7ecdb22c8e2e0d94b66f3ec87a4499b4895a586ae8cb4f70d2d23b216.json @@ -0,0 +1,23 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO data_plane_tunnels\n (id, project_id, agent_a_id, agent_b_id,\n agent_a_pubkey, agent_b_pubkey, agent_a_wg_ip, agent_b_wg_ip,\n wg_port, replica_count, status)\n VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,'pending')", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid", + "Uuid", + "Uuid", + "Uuid", + "Text", + "Text", + "Text", + "Text", + "Int4", + "Int4" + ] + }, + "nullable": [] + }, + "hash": "07bfb9d7ecdb22c8e2e0d94b66f3ec87a4499b4895a586ae8cb4f70d2d23b216" +} diff --git a/lynx/dashboard/server/.sqlx/query-0d2e54f22a7f5179e7faa3a14dfc052aa3877a3dca544d527becfddcb610bbe1.json b/lynx/dashboard/server/.sqlx/query-0d2e54f22a7f5179e7faa3a14dfc052aa3877a3dca544d527becfddcb610bbe1.json new file mode 100644 index 0000000..04434c0 --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-0d2e54f22a7f5179e7faa3a14dfc052aa3877a3dca544d527becfddcb610bbe1.json @@ -0,0 +1,116 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT id, scope, agent_id, kind, port, port_end, protocol,\n ip_list, ip_version, rate_per_min, description,\n priority, enabled, direction, created_by, created_at, updated_at\n FROM nftables_rules\n WHERE scope = 'global' AND enabled = true\n ORDER BY priority ASC\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "scope", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "agent_id", + "type_info": "Uuid" + }, + { + "ordinal": 3, + "name": "kind", + "type_info": "Text" + }, + { + "ordinal": 4, + "name": "port", + "type_info": "Int4" + }, + { + "ordinal": 5, + "name": "port_end", + "type_info": "Int4" + }, + { + "ordinal": 6, + "name": "protocol", + "type_info": "Text" + }, + { + "ordinal": 7, + "name": "ip_list", + "type_info": "TextArray" + }, + { + "ordinal": 8, + "name": "ip_version", + "type_info": "Text" + }, + { + "ordinal": 9, + "name": "rate_per_min", + "type_info": "Int4" + }, + { + "ordinal": 10, + "name": "description", + "type_info": "Text" + }, + { + "ordinal": 11, + "name": "priority", + "type_info": "Int4" + }, + { + "ordinal": 12, + "name": "enabled", + "type_info": "Bool" + }, + { + "ordinal": 13, + "name": "direction", + "type_info": "Text" + }, + { + "ordinal": 14, + "name": "created_by", + "type_info": "Uuid" + }, + { + "ordinal": 15, + "name": "created_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 16, + "name": "updated_at", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + false, + false, + true, + false, + true, + true, + true, + false, + false, + true, + true, + false, + false, + false, + true, + false, + false + ] + }, + "hash": "0d2e54f22a7f5179e7faa3a14dfc052aa3877a3dca544d527becfddcb610bbe1" +} diff --git a/lynx/dashboard/server/.sqlx/query-0d2ef2af6dd300873ac4c3ef7c79c4a3bb1758839f0ca921a87a99c97e800dea.json b/lynx/dashboard/server/.sqlx/query-0d2ef2af6dd300873ac4c3ef7c79c4a3bb1758839f0ca921a87a99c97e800dea.json new file mode 100644 index 0000000..ba9791f --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-0d2ef2af6dd300873ac4c3ef7c79c4a3bb1758839f0ca921a87a99c97e800dea.json @@ -0,0 +1,38 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT id, wg_pubkey, wg_ip::text AS wg_ip, api_port FROM agents WHERE status = 'online'", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "wg_pubkey", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "wg_ip", + "type_info": "Text" + }, + { + "ordinal": 3, + "name": "api_port", + "type_info": "Int4" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + false, + false, + false, + false + ] + }, + "hash": "0d2ef2af6dd300873ac4c3ef7c79c4a3bb1758839f0ca921a87a99c97e800dea" +} diff --git a/lynx/dashboard/server/.sqlx/query-0d42b40bf313890d096a7ba1cd15e022a898ba6d9f1cd8a5ec64e43aa8c600f8.json b/lynx/dashboard/server/.sqlx/query-0d42b40bf313890d096a7ba1cd15e022a898ba6d9f1cd8a5ec64e43aa8c600f8.json new file mode 100644 index 0000000..6f2ea35 --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-0d42b40bf313890d096a7ba1cd15e022a898ba6d9f1cd8a5ec64e43aa8c600f8.json @@ -0,0 +1,17 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE agents SET status=$1, last_heartbeat=NOW(), version=$2, arch=$3 WHERE id=$4", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Text", + "Text", + "Text", + "Uuid" + ] + }, + "nullable": [] + }, + "hash": "0d42b40bf313890d096a7ba1cd15e022a898ba6d9f1cd8a5ec64e43aa8c600f8" +} diff --git a/lynx/dashboard/server/.sqlx/query-0fc0009754fec1a23f7a214600967bc2e193c34494cc6e237cc1ee2e906ffe6d.json b/lynx/dashboard/server/.sqlx/query-0fc0009754fec1a23f7a214600967bc2e193c34494cc6e237cc1ee2e906ffe6d.json new file mode 100644 index 0000000..17c5935 --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-0fc0009754fec1a23f7a214600967bc2e193c34494cc6e237cc1ee2e906ffe6d.json @@ -0,0 +1,20 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT id FROM sessions WHERE expires_at > NOW()", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + false + ] + }, + "hash": "0fc0009754fec1a23f7a214600967bc2e193c34494cc6e237cc1ee2e906ffe6d" +} diff --git a/lynx/dashboard/server/.sqlx/query-10eeee14e3f8d26f2a71272e8de60ecd89a96d564b5e1641ee585399bd4af3ff.json b/lynx/dashboard/server/.sqlx/query-10eeee14e3f8d26f2a71272e8de60ecd89a96d564b5e1641ee585399bd4af3ff.json new file mode 100644 index 0000000..f09e746 --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-10eeee14e3f8d26f2a71272e8de60ecd89a96d564b5e1641ee585399bd4af3ff.json @@ -0,0 +1,15 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO session_logs (id, session_id, reason) VALUES ($1, $2, 'jwt_rotation')", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid", + "Uuid" + ] + }, + "nullable": [] + }, + "hash": "10eeee14e3f8d26f2a71272e8de60ecd89a96d564b5e1641ee585399bd4af3ff" +} diff --git a/lynx/dashboard/server/.sqlx/query-11e96cfd8c2736f13ce55975ea910dd68640f6f14e38a4b3342d514804e3de27.json b/lynx/dashboard/server/.sqlx/query-11e96cfd8c2736f13ce55975ea910dd68640f6f14e38a4b3342d514804e3de27.json new file mode 100644 index 0000000..74a83ea --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-11e96cfd8c2736f13ce55975ea910dd68640f6f14e38a4b3342d514804e3de27.json @@ -0,0 +1,14 @@ +{ + "db_name": "PostgreSQL", + "query": "DELETE FROM sessions WHERE id = $1", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [] + }, + "hash": "11e96cfd8c2736f13ce55975ea910dd68640f6f14e38a4b3342d514804e3de27" +} diff --git a/lynx/dashboard/server/.sqlx/query-14e00756bed3d6e262de5aa2c1130655d6dc9ba1b8c4fc33ab0158861fb424cf.json b/lynx/dashboard/server/.sqlx/query-14e00756bed3d6e262de5aa2c1130655d6dc9ba1b8c4fc33ab0158861fb424cf.json new file mode 100644 index 0000000..cacfc2d --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-14e00756bed3d6e262de5aa2c1130655d6dc9ba1b8c4fc33ab0158861fb424cf.json @@ -0,0 +1,40 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT id, wg_ip, api_port, status FROM agents WHERE id = $1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "wg_ip", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "api_port", + "type_info": "Int4" + }, + { + "ordinal": 3, + "name": "status", + "type_info": "Text" + } + ], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [ + false, + false, + false, + false + ] + }, + "hash": "14e00756bed3d6e262de5aa2c1130655d6dc9ba1b8c4fc33ab0158861fb424cf" +} diff --git a/lynx/dashboard/server/.sqlx/query-16b1751531b91212dc8da38f0a15cbb42d1be323ded00652547f942986479e33.json b/lynx/dashboard/server/.sqlx/query-16b1751531b91212dc8da38f0a15cbb42d1be323ded00652547f942986479e33.json new file mode 100644 index 0000000..b23f30e --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-16b1751531b91212dc8da38f0a15cbb42d1be323ded00652547f942986479e33.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "DELETE FROM nftables_rules WHERE id = $1 AND scope = 'global' RETURNING id", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + } + ], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [ + false + ] + }, + "hash": "16b1751531b91212dc8da38f0a15cbb42d1be323ded00652547f942986479e33" +} diff --git a/lynx/dashboard/server/.sqlx/query-16ca089285971b7cf870f6e95ac91a0785d96760b72ff6f93dad5d063f73879b.json b/lynx/dashboard/server/.sqlx/query-16ca089285971b7cf870f6e95ac91a0785d96760b72ff6f93dad5d063f73879b.json new file mode 100644 index 0000000..31a7294 --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-16ca089285971b7cf870f6e95ac91a0785d96760b72ff6f93dad5d063f73879b.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT 1 FROM agents WHERE id = $1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "?column?", + "type_info": "Int4" + } + ], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [ + null + ] + }, + "hash": "16ca089285971b7cf870f6e95ac91a0785d96760b72ff6f93dad5d063f73879b" +} diff --git a/lynx/dashboard/server/.sqlx/query-1840c87d613bff048d571a40be984385dce825047bf3dfb2c3e6022e8d05e186.json b/lynx/dashboard/server/.sqlx/query-1840c87d613bff048d571a40be984385dce825047bf3dfb2c3e6022e8d05e186.json new file mode 100644 index 0000000..e9dfa0e --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-1840c87d613bff048d571a40be984385dce825047bf3dfb2c3e6022e8d05e186.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT EXISTS(\n SELECT 1 FROM user_roles ur\n JOIN role_permissions rp ON rp.role_id = ur.role_id\n JOIN permissions p ON p.id = rp.permission_id\n WHERE ur.user_id = $1 AND p.key = '*:*'\n ) AS \"exists!\"", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "exists!", + "type_info": "Bool" + } + ], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [ + null + ] + }, + "hash": "1840c87d613bff048d571a40be984385dce825047bf3dfb2c3e6022e8d05e186" +} diff --git a/lynx/dashboard/server/.sqlx/query-189218f43aaa3e902500a8ac8fd22288348c4cdfc7ffc1cafea1a132a7a93cdb.json b/lynx/dashboard/server/.sqlx/query-189218f43aaa3e902500a8ac8fd22288348c4cdfc7ffc1cafea1a132a7a93cdb.json new file mode 100644 index 0000000..7a3cfaa --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-189218f43aaa3e902500a8ac8fd22288348c4cdfc7ffc1cafea1a132a7a93cdb.json @@ -0,0 +1,28 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT r.id, r.name FROM user_roles ur JOIN roles r ON r.id = ur.role_id WHERE ur.user_id = $1 ORDER BY r.name", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "name", + "type_info": "Text" + } + ], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [ + false, + false + ] + }, + "hash": "189218f43aaa3e902500a8ac8fd22288348c4cdfc7ffc1cafea1a132a7a93cdb" +} diff --git a/lynx/dashboard/server/.sqlx/query-1973560da6ea391a751f974fb0f0530e18bfff3efe81ef8cb2a5a5613259d229.json b/lynx/dashboard/server/.sqlx/query-1973560da6ea391a751f974fb0f0530e18bfff3efe81ef8cb2a5a5613259d229.json new file mode 100644 index 0000000..fb99bfb --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-1973560da6ea391a751f974fb0f0530e18bfff3efe81ef8cb2a5a5613259d229.json @@ -0,0 +1,52 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT id, ip, user_agent, created_at, last_used_at, expires_at\n FROM sessions\n WHERE user_id = $1 AND expires_at > NOW()\n ORDER BY last_used_at DESC\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "ip", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "user_agent", + "type_info": "Text" + }, + { + "ordinal": 3, + "name": "created_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 4, + "name": "last_used_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 5, + "name": "expires_at", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [ + false, + false, + true, + false, + false, + false + ] + }, + "hash": "1973560da6ea391a751f974fb0f0530e18bfff3efe81ef8cb2a5a5613259d229" +} diff --git a/lynx/dashboard/server/.sqlx/query-1a63477354912301499a0a1dc347c2dfc67068e56496d364d0bb1a79aaeeecb0.json b/lynx/dashboard/server/.sqlx/query-1a63477354912301499a0a1dc347c2dfc67068e56496d364d0bb1a79aaeeecb0.json new file mode 100644 index 0000000..e5e8bc1 --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-1a63477354912301499a0a1dc347c2dfc67068e56496d364d0bb1a79aaeeecb0.json @@ -0,0 +1,20 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT status FROM migration_state WHERE id = 1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "status", + "type_info": "Text" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + false + ] + }, + "hash": "1a63477354912301499a0a1dc347c2dfc67068e56496d364d0bb1a79aaeeecb0" +} diff --git a/lynx/dashboard/server/.sqlx/query-1a81592075c7e595b54bbe73962fe67deb509826b890a399e24e6c7cd8a25539.json b/lynx/dashboard/server/.sqlx/query-1a81592075c7e595b54bbe73962fe67deb509826b890a399e24e6c7cd8a25539.json new file mode 100644 index 0000000..4677101 --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-1a81592075c7e595b54bbe73962fe67deb509826b890a399e24e6c7cd8a25539.json @@ -0,0 +1,14 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE migration_state\n SET status='preparing', role='target', migration_token_hash=$1,\n started_at=NOW(), updated_at=NOW()\n WHERE id=1", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [] + }, + "hash": "1a81592075c7e595b54bbe73962fe67deb509826b890a399e24e6c7cd8a25539" +} diff --git a/lynx/dashboard/server/.sqlx/query-1a9cb381b103f7b8dc0cce6fb461fecd343164473c960e788092c987d1abda41.json b/lynx/dashboard/server/.sqlx/query-1a9cb381b103f7b8dc0cce6fb461fecd343164473c960e788092c987d1abda41.json new file mode 100644 index 0000000..87cc117 --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-1a9cb381b103f7b8dc0cce6fb461fecd343164473c960e788092c987d1abda41.json @@ -0,0 +1,26 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT id, name FROM roles ORDER BY name", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "name", + "type_info": "Text" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + false, + false + ] + }, + "hash": "1a9cb381b103f7b8dc0cce6fb461fecd343164473c960e788092c987d1abda41" +} diff --git a/lynx/dashboard/server/.sqlx/query-1cdf405ddf1eab2395fa7f406063b471cf58e07b32e008da87a8083361823bf6.json b/lynx/dashboard/server/.sqlx/query-1cdf405ddf1eab2395fa7f406063b471cf58e07b32e008da87a8083361823bf6.json new file mode 100644 index 0000000..67b99b1 --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-1cdf405ddf1eab2395fa7f406063b471cf58e07b32e008da87a8083361823bf6.json @@ -0,0 +1,14 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE users SET force_password_change = TRUE WHERE id = $1", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [] + }, + "hash": "1cdf405ddf1eab2395fa7f406063b471cf58e07b32e008da87a8083361823bf6" +} diff --git a/lynx/dashboard/server/.sqlx/query-1d9739f9d79e70530c9337aec2f5c6756157de146062cee04c9c9aa2bb332818.json b/lynx/dashboard/server/.sqlx/query-1d9739f9d79e70530c9337aec2f5c6756157de146062cee04c9c9aa2bb332818.json new file mode 100644 index 0000000..7aef988 --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-1d9739f9d79e70530c9337aec2f5c6756157de146062cee04c9c9aa2bb332818.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT EXISTS(SELECT 1 FROM users WHERE username = $1)", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "exists", + "type_info": "Bool" + } + ], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [ + null + ] + }, + "hash": "1d9739f9d79e70530c9337aec2f5c6756157de146062cee04c9c9aa2bb332818" +} diff --git a/lynx/dashboard/server/.sqlx/query-1e231d0b76bde0641c1779d2c270737d1e1de1d5731c0d19995352bc6b05a4d3.json b/lynx/dashboard/server/.sqlx/query-1e231d0b76bde0641c1779d2c270737d1e1de1d5731c0d19995352bc6b05a4d3.json new file mode 100644 index 0000000..eed5f75 --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-1e231d0b76bde0641c1779d2c270737d1e1de1d5731c0d19995352bc6b05a4d3.json @@ -0,0 +1,28 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT theme, locale FROM user_preferences WHERE user_id = $1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "theme", + "type_info": "Text" + }, + { + "ordinal": 1, + "name": "locale", + "type_info": "Text" + } + ], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [ + false, + false + ] + }, + "hash": "1e231d0b76bde0641c1779d2c270737d1e1de1d5731c0d19995352bc6b05a4d3" +} diff --git a/lynx/dashboard/server/.sqlx/query-1f09b98d0fd53bb773b3b52af62db3d9e9c3bf98cdee3707981914c9198f244e.json b/lynx/dashboard/server/.sqlx/query-1f09b98d0fd53bb773b3b52af62db3d9e9c3bf98cdee3707981914c9198f244e.json new file mode 100644 index 0000000..d58f1b0 --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-1f09b98d0fd53bb773b3b52af62db3d9e9c3bf98cdee3707981914c9198f244e.json @@ -0,0 +1,14 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE domain_config SET domain=$1, status='pending', error_message=NULL, updated_at=NOW() WHERE id=1", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [] + }, + "hash": "1f09b98d0fd53bb773b3b52af62db3d9e9c3bf98cdee3707981914c9198f244e" +} diff --git a/lynx/dashboard/server/.sqlx/query-1f5b0342ef80f28dd38c3fe69f0211f43fc7a7d208aae700a1972da82b7b6eed.json b/lynx/dashboard/server/.sqlx/query-1f5b0342ef80f28dd38c3fe69f0211f43fc7a7d208aae700a1972da82b7b6eed.json new file mode 100644 index 0000000..6fe61db --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-1f5b0342ef80f28dd38c3fe69f0211f43fc7a7d208aae700a1972da82b7b6eed.json @@ -0,0 +1,15 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO session_logs (id, session_id, reason) VALUES ($1, $2, 'mass_logout')", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid", + "Uuid" + ] + }, + "nullable": [] + }, + "hash": "1f5b0342ef80f28dd38c3fe69f0211f43fc7a7d208aae700a1972da82b7b6eed" +} diff --git a/lynx/dashboard/server/.sqlx/query-204b794f714f283b07b6fe54b9005f24f8e9c352176ee693a2a5d296ecfbce29.json b/lynx/dashboard/server/.sqlx/query-204b794f714f283b07b6fe54b9005f24f8e9c352176ee693a2a5d296ecfbce29.json new file mode 100644 index 0000000..454dc54 --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-204b794f714f283b07b6fe54b9005f24f8e9c352176ee693a2a5d296ecfbce29.json @@ -0,0 +1,26 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT id, key FROM permissions ORDER BY key", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "key", + "type_info": "Text" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + false, + false + ] + }, + "hash": "204b794f714f283b07b6fe54b9005f24f8e9c352176ee693a2a5d296ecfbce29" +} diff --git a/lynx/dashboard/server/.sqlx/query-211aa911356904f3ec992e00595cf624be631894dcb2c75755c550421e067a40.json b/lynx/dashboard/server/.sqlx/query-211aa911356904f3ec992e00595cf624be631894dcb2c75755c550421e067a40.json new file mode 100644 index 0000000..29d53d8 --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-211aa911356904f3ec992e00595cf624be631894dcb2c75755c550421e067a40.json @@ -0,0 +1,20 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT id FROM permissions WHERE key = '*:*'", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + false + ] + }, + "hash": "211aa911356904f3ec992e00595cf624be631894dcb2c75755c550421e067a40" +} diff --git a/lynx/dashboard/server/.sqlx/query-21af8cca164cad6378377f035ec42e2cac73a91cb0cb36ff4c40b6ccc85e15ae.json b/lynx/dashboard/server/.sqlx/query-21af8cca164cad6378377f035ec42e2cac73a91cb0cb36ff4c40b6ccc85e15ae.json new file mode 100644 index 0000000..0bcb4f7 --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-21af8cca164cad6378377f035ec42e2cac73a91cb0cb36ff4c40b6ccc85e15ae.json @@ -0,0 +1,12 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE migration_state SET status='completed', completed_at=NOW(), updated_at=NOW() WHERE id=1", + "describe": { + "columns": [], + "parameters": { + "Left": [] + }, + "nullable": [] + }, + "hash": "21af8cca164cad6378377f035ec42e2cac73a91cb0cb36ff4c40b6ccc85e15ae" +} diff --git a/lynx/dashboard/server/.sqlx/query-265bd5e210dd30803c2bbfb22d2504e69b17ebf5a314bdb9fb64c15e21af1cad.json b/lynx/dashboard/server/.sqlx/query-265bd5e210dd30803c2bbfb22d2504e69b17ebf5a314bdb9fb64c15e21af1cad.json new file mode 100644 index 0000000..ec3a4a9 --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-265bd5e210dd30803c2bbfb22d2504e69b17ebf5a314bdb9fb64c15e21af1cad.json @@ -0,0 +1,129 @@ +{ + "db_name": "PostgreSQL", + "query": "\n INSERT INTO nftables_rules\n (id, scope, agent_id, kind, port, port_end, protocol, ip_list, ip_version,\n rate_per_min, description, priority, direction, created_by)\n VALUES ($1, 'global', NULL, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)\n RETURNING id, scope, agent_id, kind, port, port_end, protocol,\n ip_list, ip_version, rate_per_min, description,\n priority, enabled, direction, created_by, created_at, updated_at\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "scope", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "agent_id", + "type_info": "Uuid" + }, + { + "ordinal": 3, + "name": "kind", + "type_info": "Text" + }, + { + "ordinal": 4, + "name": "port", + "type_info": "Int4" + }, + { + "ordinal": 5, + "name": "port_end", + "type_info": "Int4" + }, + { + "ordinal": 6, + "name": "protocol", + "type_info": "Text" + }, + { + "ordinal": 7, + "name": "ip_list", + "type_info": "TextArray" + }, + { + "ordinal": 8, + "name": "ip_version", + "type_info": "Text" + }, + { + "ordinal": 9, + "name": "rate_per_min", + "type_info": "Int4" + }, + { + "ordinal": 10, + "name": "description", + "type_info": "Text" + }, + { + "ordinal": 11, + "name": "priority", + "type_info": "Int4" + }, + { + "ordinal": 12, + "name": "enabled", + "type_info": "Bool" + }, + { + "ordinal": 13, + "name": "direction", + "type_info": "Text" + }, + { + "ordinal": 14, + "name": "created_by", + "type_info": "Uuid" + }, + { + "ordinal": 15, + "name": "created_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 16, + "name": "updated_at", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [ + "Uuid", + "Text", + "Int4", + "Int4", + "Text", + "TextArray", + "Text", + "Int4", + "Text", + "Int4", + "Text", + "Uuid" + ] + }, + "nullable": [ + false, + false, + true, + false, + true, + true, + true, + false, + false, + true, + true, + false, + false, + false, + true, + false, + false + ] + }, + "hash": "265bd5e210dd30803c2bbfb22d2504e69b17ebf5a314bdb9fb64c15e21af1cad" +} diff --git a/lynx/dashboard/server/.sqlx/query-26da9efd448c979af02634d0ac3b7280ade8af8dea96a1237732317c32643c35.json b/lynx/dashboard/server/.sqlx/query-26da9efd448c979af02634d0ac3b7280ade8af8dea96a1237732317c32643c35.json new file mode 100644 index 0000000..da6b91c --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-26da9efd448c979af02634d0ac3b7280ade8af8dea96a1237732317c32643c35.json @@ -0,0 +1,17 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO agent_events (id, agent_id, event, detail) VALUES ($1, $2, $3, $4)", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid", + "Uuid", + "Text", + "Text" + ] + }, + "nullable": [] + }, + "hash": "26da9efd448c979af02634d0ac3b7280ade8af8dea96a1237732317c32643c35" +} diff --git a/lynx/dashboard/server/.sqlx/query-27170bba17f0a3741d6675957c6e6d25ecec938882b5221440a0797c67eb3c36.json b/lynx/dashboard/server/.sqlx/query-27170bba17f0a3741d6675957c6e6d25ecec938882b5221440a0797c67eb3c36.json new file mode 100644 index 0000000..98b306a --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-27170bba17f0a3741d6675957c6e6d25ecec938882b5221440a0797c67eb3c36.json @@ -0,0 +1,15 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO agent_events (id, agent_id, event, detail) VALUES ($1, $2, 'disconnected', NULL)", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid", + "Uuid" + ] + }, + "nullable": [] + }, + "hash": "27170bba17f0a3741d6675957c6e6d25ecec938882b5221440a0797c67eb3c36" +} diff --git a/lynx/dashboard/server/.sqlx/query-28b24383bd086639f396e2bb59ae7620ef22bc6083c74a810ba4017c92ba7fd3.json b/lynx/dashboard/server/.sqlx/query-28b24383bd086639f396e2bb59ae7620ef22bc6083c74a810ba4017c92ba7fd3.json new file mode 100644 index 0000000..608e27a --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-28b24383bd086639f396e2bb59ae7620ef22bc6083c74a810ba4017c92ba7fd3.json @@ -0,0 +1,17 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO agent_events (id, agent_id, event, detail, created_at) VALUES ($1, $2, $3, $4, NOW())", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid", + "Uuid", + "Text", + "Text" + ] + }, + "nullable": [] + }, + "hash": "28b24383bd086639f396e2bb59ae7620ef22bc6083c74a810ba4017c92ba7fd3" +} diff --git a/lynx/dashboard/server/.sqlx/query-2927ed8c7aee5fc5b89fc57d3560d82f6f10ed965f9f30bd872dfb94e7377080.json b/lynx/dashboard/server/.sqlx/query-2927ed8c7aee5fc5b89fc57d3560d82f6f10ed965f9f30bd872dfb94e7377080.json new file mode 100644 index 0000000..2ac5dd1 --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-2927ed8c7aee5fc5b89fc57d3560d82f6f10ed965f9f30bd872dfb94e7377080.json @@ -0,0 +1,12 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE migration_state SET status='aborted', updated_at=NOW() WHERE id=1", + "describe": { + "columns": [], + "parameters": { + "Left": [] + }, + "nullable": [] + }, + "hash": "2927ed8c7aee5fc5b89fc57d3560d82f6f10ed965f9f30bd872dfb94e7377080" +} diff --git a/lynx/dashboard/server/.sqlx/query-2a4fdddd31c59d96ebce2d15307c1b681ea95e5ccfd77691b07aad740f17b932.json b/lynx/dashboard/server/.sqlx/query-2a4fdddd31c59d96ebce2d15307c1b681ea95e5ccfd77691b07aad740f17b932.json new file mode 100644 index 0000000..7b80fd0 --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-2a4fdddd31c59d96ebce2d15307c1b681ea95e5ccfd77691b07aad740f17b932.json @@ -0,0 +1,12 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE migration_state SET status='waiting_agents', updated_at=NOW() WHERE id=1", + "describe": { + "columns": [], + "parameters": { + "Left": [] + }, + "nullable": [] + }, + "hash": "2a4fdddd31c59d96ebce2d15307c1b681ea95e5ccfd77691b07aad740f17b932" +} diff --git a/lynx/dashboard/server/.sqlx/query-2c30e55d43c427acce4c7f21d86435b7e4fcf5b0252d8b73f0692ff1e20b99af.json b/lynx/dashboard/server/.sqlx/query-2c30e55d43c427acce4c7f21d86435b7e4fcf5b0252d8b73f0692ff1e20b99af.json new file mode 100644 index 0000000..c762891 --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-2c30e55d43c427acce4c7f21d86435b7e4fcf5b0252d8b73f0692ff1e20b99af.json @@ -0,0 +1,14 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE data_plane_tunnels SET status='active', updated_at=NOW() WHERE id=$1", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [] + }, + "hash": "2c30e55d43c427acce4c7f21d86435b7e4fcf5b0252d8b73f0692ff1e20b99af" +} diff --git a/lynx/dashboard/server/.sqlx/query-2d03c5c6a574aab90998a1b5635492733fcfbb71b7846eadd50bf8819b0fdfe8.json b/lynx/dashboard/server/.sqlx/query-2d03c5c6a574aab90998a1b5635492733fcfbb71b7846eadd50bf8819b0fdfe8.json new file mode 100644 index 0000000..91d4c06 --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-2d03c5c6a574aab90998a1b5635492733fcfbb71b7846eadd50bf8819b0fdfe8.json @@ -0,0 +1,44 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT id, triggered_by, reason, scope, created_at\n FROM rotation_log\n ORDER BY created_at DESC\n LIMIT 50\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "triggered_by", + "type_info": "Uuid" + }, + { + "ordinal": 2, + "name": "reason", + "type_info": "Text" + }, + { + "ordinal": 3, + "name": "scope", + "type_info": "Text" + }, + { + "ordinal": 4, + "name": "created_at", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + false, + true, + false, + false, + false + ] + }, + "hash": "2d03c5c6a574aab90998a1b5635492733fcfbb71b7846eadd50bf8819b0fdfe8" +} diff --git a/lynx/dashboard/server/.sqlx/query-2d7fdc4a481f272f3cbb28dfa84cabc82859bdfc2e1a79cdc60f3bc6d2a7ce21.json b/lynx/dashboard/server/.sqlx/query-2d7fdc4a481f272f3cbb28dfa84cabc82859bdfc2e1a79cdc60f3bc6d2a7ce21.json new file mode 100644 index 0000000..f37c70a --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-2d7fdc4a481f272f3cbb28dfa84cabc82859bdfc2e1a79cdc60f3bc6d2a7ce21.json @@ -0,0 +1,12 @@ +{ + "db_name": "PostgreSQL", + "query": "\n INSERT INTO system_config (key, value)\n VALUES ('setup_token_issued_at', NOW()::text)\n ON CONFLICT (key) DO NOTHING\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [] + }, + "nullable": [] + }, + "hash": "2d7fdc4a481f272f3cbb28dfa84cabc82859bdfc2e1a79cdc60f3bc6d2a7ce21" +} diff --git a/lynx/dashboard/server/.sqlx/query-31c8b54aba4d9bd08ea2188597246120e504efae5eb87989b47a274b76dcacfb.json b/lynx/dashboard/server/.sqlx/query-31c8b54aba4d9bd08ea2188597246120e504efae5eb87989b47a274b76dcacfb.json new file mode 100644 index 0000000..6436b7c --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-31c8b54aba4d9bd08ea2188597246120e504efae5eb87989b47a274b76dcacfb.json @@ -0,0 +1,14 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE agents SET status='online', last_heartbeat=NOW() WHERE id=$1", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [] + }, + "hash": "31c8b54aba4d9bd08ea2188597246120e504efae5eb87989b47a274b76dcacfb" +} diff --git a/lynx/dashboard/server/.sqlx/query-331133cf9c129cf4e8c7e12b7ea78d891846595d320ca9faf858df753b995681.json b/lynx/dashboard/server/.sqlx/query-331133cf9c129cf4e8c7e12b7ea78d891846595d320ca9faf858df753b995681.json new file mode 100644 index 0000000..09d620f --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-331133cf9c129cf4e8c7e12b7ea78d891846595d320ca9faf858df753b995681.json @@ -0,0 +1,32 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT id, wg_ip::text AS wg_ip, api_port FROM agents WHERE is_local_agent = true LIMIT 1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "wg_ip", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "api_port", + "type_info": "Int4" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + false, + false, + false + ] + }, + "hash": "331133cf9c129cf4e8c7e12b7ea78d891846595d320ca9faf858df753b995681" +} diff --git a/lynx/dashboard/server/.sqlx/query-377a98cd59592dbf445eaae12d4ac913d9e837876c86deafa56cfc4816d648b1.json b/lynx/dashboard/server/.sqlx/query-377a98cd59592dbf445eaae12d4ac913d9e837876c86deafa56cfc4816d648b1.json new file mode 100644 index 0000000..3729815 --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-377a98cd59592dbf445eaae12d4ac913d9e837876c86deafa56cfc4816d648b1.json @@ -0,0 +1,50 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT id, name, status, wg_ip, version, last_heartbeat FROM agents ORDER BY created_at ASC", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "name", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "status", + "type_info": "Text" + }, + { + "ordinal": 3, + "name": "wg_ip", + "type_info": "Text" + }, + { + "ordinal": 4, + "name": "version", + "type_info": "Text" + }, + { + "ordinal": 5, + "name": "last_heartbeat", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + false, + false, + false, + false, + true, + true + ] + }, + "hash": "377a98cd59592dbf445eaae12d4ac913d9e837876c86deafa56cfc4816d648b1" +} diff --git a/lynx/dashboard/server/.sqlx/query-37839ae3a690f52077fe56fd012a361929bfc0d90c3e13f387ec5bb7c8a25fcb.json b/lynx/dashboard/server/.sqlx/query-37839ae3a690f52077fe56fd012a361929bfc0d90c3e13f387ec5bb7c8a25fcb.json new file mode 100644 index 0000000..89b8ba7 --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-37839ae3a690f52077fe56fd012a361929bfc0d90c3e13f387ec5bb7c8a25fcb.json @@ -0,0 +1,118 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT id, scope, agent_id, kind, port, port_end, protocol,\n ip_list, ip_version, rate_per_min, description,\n priority, enabled, direction, created_by, created_at, updated_at\n FROM nftables_rules\n WHERE scope = 'local' AND agent_id = $1 AND enabled = true\n ORDER BY priority ASC\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "scope", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "agent_id", + "type_info": "Uuid" + }, + { + "ordinal": 3, + "name": "kind", + "type_info": "Text" + }, + { + "ordinal": 4, + "name": "port", + "type_info": "Int4" + }, + { + "ordinal": 5, + "name": "port_end", + "type_info": "Int4" + }, + { + "ordinal": 6, + "name": "protocol", + "type_info": "Text" + }, + { + "ordinal": 7, + "name": "ip_list", + "type_info": "TextArray" + }, + { + "ordinal": 8, + "name": "ip_version", + "type_info": "Text" + }, + { + "ordinal": 9, + "name": "rate_per_min", + "type_info": "Int4" + }, + { + "ordinal": 10, + "name": "description", + "type_info": "Text" + }, + { + "ordinal": 11, + "name": "priority", + "type_info": "Int4" + }, + { + "ordinal": 12, + "name": "enabled", + "type_info": "Bool" + }, + { + "ordinal": 13, + "name": "direction", + "type_info": "Text" + }, + { + "ordinal": 14, + "name": "created_by", + "type_info": "Uuid" + }, + { + "ordinal": 15, + "name": "created_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 16, + "name": "updated_at", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [ + false, + false, + true, + false, + true, + true, + true, + false, + false, + true, + true, + false, + false, + false, + true, + false, + false + ] + }, + "hash": "37839ae3a690f52077fe56fd012a361929bfc0d90c3e13f387ec5bb7c8a25fcb" +} diff --git a/lynx/dashboard/server/.sqlx/query-37d4ef396e93339202bc64ba7eefbea379b9978eedd006403e090600d3f4680f.json b/lynx/dashboard/server/.sqlx/query-37d4ef396e93339202bc64ba7eefbea379b9978eedd006403e090600d3f4680f.json new file mode 100644 index 0000000..4e2bb8a --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-37d4ef396e93339202bc64ba7eefbea379b9978eedd006403e090600d3f4680f.json @@ -0,0 +1,23 @@ +{ + "db_name": "PostgreSQL", + "query": "\n INSERT INTO audit_log (\n id, agent_id, organization_id, user_id, command_type,\n result, error, previous_hash, entry_hash, created_at\n ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)\n ON CONFLICT (id) DO NOTHING\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid", + "Uuid", + "Uuid", + "Uuid", + "Text", + "Text", + "Text", + "Text", + "Text", + "Timestamptz" + ] + }, + "nullable": [] + }, + "hash": "37d4ef396e93339202bc64ba7eefbea379b9978eedd006403e090600d3f4680f" +} diff --git a/lynx/dashboard/server/.sqlx/query-38331e94fd07618f293c2174459445b5edaa81cf2a527abcbd8650360c261927.json b/lynx/dashboard/server/.sqlx/query-38331e94fd07618f293c2174459445b5edaa81cf2a527abcbd8650360c261927.json new file mode 100644 index 0000000..4c7e194 --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-38331e94fd07618f293c2174459445b5edaa81cf2a527abcbd8650360c261927.json @@ -0,0 +1,20 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT domain FROM domain_config WHERE id = 1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "domain", + "type_info": "Text" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + true + ] + }, + "hash": "38331e94fd07618f293c2174459445b5edaa81cf2a527abcbd8650360c261927" +} diff --git a/lynx/dashboard/server/.sqlx/query-388de60f218494e05573b58846255397c6993a77ebdc93fd0c92394b3655012e.json b/lynx/dashboard/server/.sqlx/query-388de60f218494e05573b58846255397c6993a77ebdc93fd0c92394b3655012e.json new file mode 100644 index 0000000..84694ed --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-388de60f218494e05573b58846255397c6993a77ebdc93fd0c92394b3655012e.json @@ -0,0 +1,16 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO user_preferences (user_id, theme, locale)\n VALUES ($1, COALESCE($2, 'system'), COALESCE($3, 'en'))\n ON CONFLICT (user_id) DO UPDATE SET\n theme = COALESCE($2, user_preferences.theme),\n locale = COALESCE($3, user_preferences.locale),\n updated_at = NOW()", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid", + "Text", + "Text" + ] + }, + "nullable": [] + }, + "hash": "388de60f218494e05573b58846255397c6993a77ebdc93fd0c92394b3655012e" +} diff --git a/lynx/dashboard/server/.sqlx/query-3a9e69b7c9cae72e08c7fb8094b086789094599f575a6d27b2ad7e15cb71eac6.json b/lynx/dashboard/server/.sqlx/query-3a9e69b7c9cae72e08c7fb8094b086789094599f575a6d27b2ad7e15cb71eac6.json new file mode 100644 index 0000000..dac9a44 --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-3a9e69b7c9cae72e08c7fb8094b086789094599f575a6d27b2ad7e15cb71eac6.json @@ -0,0 +1,34 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT wg_ip, api_port, status FROM agents WHERE id=$1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "wg_ip", + "type_info": "Text" + }, + { + "ordinal": 1, + "name": "api_port", + "type_info": "Int4" + }, + { + "ordinal": 2, + "name": "status", + "type_info": "Text" + } + ], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [ + false, + false, + false + ] + }, + "hash": "3a9e69b7c9cae72e08c7fb8094b086789094599f575a6d27b2ad7e15cb71eac6" +} diff --git a/lynx/dashboard/server/.sqlx/query-3c8140984103bed6d663024e9c846250ed9077ff59f33e4c285c21ffb5d3c031.json b/lynx/dashboard/server/.sqlx/query-3c8140984103bed6d663024e9c846250ed9077ff59f33e4c285c21ffb5d3c031.json new file mode 100644 index 0000000..ba5e1dd --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-3c8140984103bed6d663024e9c846250ed9077ff59f33e4c285c21ffb5d3c031.json @@ -0,0 +1,18 @@ +{ + "db_name": "PostgreSQL", + "query": "\n INSERT INTO white_label (id, company_name, logo_url, primary_color, secondary_color, accent_color, updated_at)\n VALUES (1,\n COALESCE($1, 'Lynx'),\n $2,\n COALESCE($3, '#0f172a'),\n COALESCE($4, '#38bdf8'),\n COALESCE($5, '#6366f1'),\n NOW()\n )\n ON CONFLICT (id) DO UPDATE SET\n company_name = COALESCE($1, white_label.company_name),\n logo_url = COALESCE($2, white_label.logo_url),\n primary_color = COALESCE($3, white_label.primary_color),\n secondary_color = COALESCE($4, white_label.secondary_color),\n accent_color = COALESCE($5, white_label.accent_color),\n updated_at = NOW()\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Text", + "Text", + "Text", + "Text", + "Text" + ] + }, + "nullable": [] + }, + "hash": "3c8140984103bed6d663024e9c846250ed9077ff59f33e4c285c21ffb5d3c031" +} diff --git a/lynx/dashboard/server/.sqlx/query-3cd7469d8dd28de161b63eebba41c039d9f3eaf3267879544f8f6fb615a0fafa.json b/lynx/dashboard/server/.sqlx/query-3cd7469d8dd28de161b63eebba41c039d9f3eaf3267879544f8f6fb615a0fafa.json new file mode 100644 index 0000000..369c60e --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-3cd7469d8dd28de161b63eebba41c039d9f3eaf3267879544f8f6fb615a0fafa.json @@ -0,0 +1,15 @@ +{ + "db_name": "PostgreSQL", + "query": "DELETE FROM organizations WHERE id = $1 AND owner_id = $2", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid", + "Uuid" + ] + }, + "nullable": [] + }, + "hash": "3cd7469d8dd28de161b63eebba41c039d9f3eaf3267879544f8f6fb615a0fafa" +} diff --git a/lynx/dashboard/server/.sqlx/query-3e6e8301c56e857fab3918b31b9e73133f690d14743015bffeaf4c26c63fbb94.json b/lynx/dashboard/server/.sqlx/query-3e6e8301c56e857fab3918b31b9e73133f690d14743015bffeaf4c26c63fbb94.json new file mode 100644 index 0000000..e4ef779 --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-3e6e8301c56e857fab3918b31b9e73133f690d14743015bffeaf4c26c63fbb94.json @@ -0,0 +1,12 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE domain_config SET port_19443_open=false, updated_at=NOW() WHERE id=1", + "describe": { + "columns": [], + "parameters": { + "Left": [] + }, + "nullable": [] + }, + "hash": "3e6e8301c56e857fab3918b31b9e73133f690d14743015bffeaf4c26c63fbb94" +} diff --git a/lynx/dashboard/server/.sqlx/query-3f37bcaf4d27e480d7c46f9325a4262a37c02a062a804f08e4ff8a7e7c3c395c.json b/lynx/dashboard/server/.sqlx/query-3f37bcaf4d27e480d7c46f9325a4262a37c02a062a804f08e4ff8a7e7c3c395c.json new file mode 100644 index 0000000..e781aa4 --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-3f37bcaf4d27e480d7c46f9325a4262a37c02a062a804f08e4ff8a7e7c3c395c.json @@ -0,0 +1,17 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO user_roles (id, user_id, role_id, created_by) VALUES ($1, $2, $3, $4)\n ON CONFLICT (user_id, role_id) DO NOTHING", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid", + "Uuid", + "Uuid", + "Uuid" + ] + }, + "nullable": [] + }, + "hash": "3f37bcaf4d27e480d7c46f9325a4262a37c02a062a804f08e4ff8a7e7c3c395c" +} diff --git a/lynx/dashboard/server/.sqlx/query-412b66ba62bd41ff74d08cc5075673d717f0fe9f7d40f8cdeef4956dc0a63318.json b/lynx/dashboard/server/.sqlx/query-412b66ba62bd41ff74d08cc5075673d717f0fe9f7d40f8cdeef4956dc0a63318.json new file mode 100644 index 0000000..f3dd600 --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-412b66ba62bd41ff74d08cc5075673d717f0fe9f7d40f8cdeef4956dc0a63318.json @@ -0,0 +1,34 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT id, wg_ip, api_port FROM agents WHERE id = $1 AND status = 'online'", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "wg_ip", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "api_port", + "type_info": "Int4" + } + ], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [ + false, + false, + false + ] + }, + "hash": "412b66ba62bd41ff74d08cc5075673d717f0fe9f7d40f8cdeef4956dc0a63318" +} diff --git a/lynx/dashboard/server/.sqlx/query-463e3cb3cc41990e508d9159e6e4043629edcc6761ce8ccaddfafc51523b2991.json b/lynx/dashboard/server/.sqlx/query-463e3cb3cc41990e508d9159e6e4043629edcc6761ce8ccaddfafc51523b2991.json new file mode 100644 index 0000000..6cb1b87 --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-463e3cb3cc41990e508d9159e6e4043629edcc6761ce8ccaddfafc51523b2991.json @@ -0,0 +1,14 @@ +{ + "db_name": "PostgreSQL", + "query": "DELETE FROM roles WHERE id = $1", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [] + }, + "hash": "463e3cb3cc41990e508d9159e6e4043629edcc6761ce8ccaddfafc51523b2991" +} diff --git a/lynx/dashboard/server/.sqlx/query-476c825437be3dcacbe3fd880af94763f6c5e572fac927159c22449ee66e274b.json b/lynx/dashboard/server/.sqlx/query-476c825437be3dcacbe3fd880af94763f6c5e572fac927159c22449ee66e274b.json new file mode 100644 index 0000000..927be5e --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-476c825437be3dcacbe3fd880af94763f6c5e572fac927159c22449ee66e274b.json @@ -0,0 +1,15 @@ +{ + "db_name": "PostgreSQL", + "query": "DELETE FROM organization_members WHERE organization_id = $1 AND user_id = $2", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid", + "Uuid" + ] + }, + "nullable": [] + }, + "hash": "476c825437be3dcacbe3fd880af94763f6c5e572fac927159c22449ee66e274b" +} diff --git a/lynx/dashboard/server/.sqlx/query-49ec05b4abcd978617b54d6b313d7b7e51a1cf18461b75eef85b1e53708e0ce1.json b/lynx/dashboard/server/.sqlx/query-49ec05b4abcd978617b54d6b313d7b7e51a1cf18461b75eef85b1e53708e0ce1.json new file mode 100644 index 0000000..1ece34a --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-49ec05b4abcd978617b54d6b313d7b7e51a1cf18461b75eef85b1e53708e0ce1.json @@ -0,0 +1,15 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO roles (id, name, created_by) VALUES ($1, 'Admin', $2)", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid", + "Uuid" + ] + }, + "nullable": [] + }, + "hash": "49ec05b4abcd978617b54d6b313d7b7e51a1cf18461b75eef85b1e53708e0ce1" +} diff --git a/lynx/dashboard/server/.sqlx/query-4b080e91cd13b16c5820a9cabdaf1b6f00f7c660a43df4867ffb733bac029461.json b/lynx/dashboard/server/.sqlx/query-4b080e91cd13b16c5820a9cabdaf1b6f00f7c660a43df4867ffb733bac029461.json new file mode 100644 index 0000000..666b775 --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-4b080e91cd13b16c5820a9cabdaf1b6f00f7c660a43df4867ffb733bac029461.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT COUNT(*) FROM audit_log WHERE agent_id = $1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "count", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [ + null + ] + }, + "hash": "4b080e91cd13b16c5820a9cabdaf1b6f00f7c660a43df4867ffb733bac029461" +} diff --git a/lynx/dashboard/server/.sqlx/query-4bf02ac6e05fb3a91e639d0b20c0f0c3c6b30e5c936e285d2b8c2ead4ec2a466.json b/lynx/dashboard/server/.sqlx/query-4bf02ac6e05fb3a91e639d0b20c0f0c3c6b30e5c936e285d2b8c2ead4ec2a466.json new file mode 100644 index 0000000..45e8389 --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-4bf02ac6e05fb3a91e639d0b20c0f0c3c6b30e5c936e285d2b8c2ead4ec2a466.json @@ -0,0 +1,16 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO session_logs (id, session_id, reason) VALUES ($1, $2, $3)", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid", + "Uuid", + "Text" + ] + }, + "nullable": [] + }, + "hash": "4bf02ac6e05fb3a91e639d0b20c0f0c3c6b30e5c936e285d2b8c2ead4ec2a466" +} diff --git a/lynx/dashboard/server/.sqlx/query-4f572f12c9c07f76b598f346b3c926eb6e1a2cdc4f9d59b7211f54041ccafe7d.json b/lynx/dashboard/server/.sqlx/query-4f572f12c9c07f76b598f346b3c926eb6e1a2cdc4f9d59b7211f54041ccafe7d.json new file mode 100644 index 0000000..d58f28b --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-4f572f12c9c07f76b598f346b3c926eb6e1a2cdc4f9d59b7211f54041ccafe7d.json @@ -0,0 +1,15 @@ +{ + "db_name": "PostgreSQL", + "query": "\n INSERT INTO global_rule_sync (rule_id, agent_id, synced_at)\n VALUES ($1, $2, NOW())\n ON CONFLICT (rule_id, agent_id) DO UPDATE SET synced_at = NOW()\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid", + "Uuid" + ] + }, + "nullable": [] + }, + "hash": "4f572f12c9c07f76b598f346b3c926eb6e1a2cdc4f9d59b7211f54041ccafe7d" +} diff --git a/lynx/dashboard/server/.sqlx/query-4f863def6d4a9994282dd10a4fb845f09c114022f64afc980a6538c0f22bc1f8.json b/lynx/dashboard/server/.sqlx/query-4f863def6d4a9994282dd10a4fb845f09c114022f64afc980a6538c0f22bc1f8.json new file mode 100644 index 0000000..fcc3f88 --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-4f863def6d4a9994282dd10a4fb845f09c114022f64afc980a6538c0f22bc1f8.json @@ -0,0 +1,118 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT id, scope, agent_id, kind, port, port_end, protocol,\n ip_list, ip_version, rate_per_min, description,\n priority, enabled, direction, created_by, created_at, updated_at\n FROM nftables_rules\n WHERE scope = 'local' AND agent_id = $1\n ORDER BY priority ASC, created_at ASC\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "scope", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "agent_id", + "type_info": "Uuid" + }, + { + "ordinal": 3, + "name": "kind", + "type_info": "Text" + }, + { + "ordinal": 4, + "name": "port", + "type_info": "Int4" + }, + { + "ordinal": 5, + "name": "port_end", + "type_info": "Int4" + }, + { + "ordinal": 6, + "name": "protocol", + "type_info": "Text" + }, + { + "ordinal": 7, + "name": "ip_list", + "type_info": "TextArray" + }, + { + "ordinal": 8, + "name": "ip_version", + "type_info": "Text" + }, + { + "ordinal": 9, + "name": "rate_per_min", + "type_info": "Int4" + }, + { + "ordinal": 10, + "name": "description", + "type_info": "Text" + }, + { + "ordinal": 11, + "name": "priority", + "type_info": "Int4" + }, + { + "ordinal": 12, + "name": "enabled", + "type_info": "Bool" + }, + { + "ordinal": 13, + "name": "direction", + "type_info": "Text" + }, + { + "ordinal": 14, + "name": "created_by", + "type_info": "Uuid" + }, + { + "ordinal": 15, + "name": "created_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 16, + "name": "updated_at", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [ + false, + false, + true, + false, + true, + true, + true, + false, + false, + true, + true, + false, + false, + false, + true, + false, + false + ] + }, + "hash": "4f863def6d4a9994282dd10a4fb845f09c114022f64afc980a6538c0f22bc1f8" +} diff --git a/lynx/dashboard/server/.sqlx/query-4fb5bb08cece498a68856a0e3009289b985ee3016743934dfeb3e17afe243d17.json b/lynx/dashboard/server/.sqlx/query-4fb5bb08cece498a68856a0e3009289b985ee3016743934dfeb3e17afe243d17.json new file mode 100644 index 0000000..2bea0bf --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-4fb5bb08cece498a68856a0e3009289b985ee3016743934dfeb3e17afe243d17.json @@ -0,0 +1,32 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT status, agents_total, agents_confirmed FROM migration_state WHERE id=1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "status", + "type_info": "Text" + }, + { + "ordinal": 1, + "name": "agents_total", + "type_info": "Int4" + }, + { + "ordinal": 2, + "name": "agents_confirmed", + "type_info": "Int4" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + false, + false, + false + ] + }, + "hash": "4fb5bb08cece498a68856a0e3009289b985ee3016743934dfeb3e17afe243d17" +} diff --git a/lynx/dashboard/server/.sqlx/query-50293c2e54af11d4c2a553e29b671cef087a159c6ee7182d8ca929ecb748f3b7.json b/lynx/dashboard/server/.sqlx/query-50293c2e54af11d4c2a553e29b671cef087a159c6ee7182d8ca929ecb748f3b7.json new file mode 100644 index 0000000..f62678a --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-50293c2e54af11d4c2a553e29b671cef087a159c6ee7182d8ca929ecb748f3b7.json @@ -0,0 +1,14 @@ +{ + "db_name": "PostgreSQL", + "query": "DELETE FROM users WHERE id = $1", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [] + }, + "hash": "50293c2e54af11d4c2a553e29b671cef087a159c6ee7182d8ca929ecb748f3b7" +} diff --git a/lynx/dashboard/server/.sqlx/query-50f8e32fc7290f7c69685700e89ea70d58725b3761a08ce1e26e5a5afca6549d.json b/lynx/dashboard/server/.sqlx/query-50f8e32fc7290f7c69685700e89ea70d58725b3761a08ce1e26e5a5afca6549d.json new file mode 100644 index 0000000..fd029d7 --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-50f8e32fc7290f7c69685700e89ea70d58725b3761a08ce1e26e5a5afca6549d.json @@ -0,0 +1,19 @@ +{ + "db_name": "PostgreSQL", + "query": "\n INSERT INTO update_log (id, triggered_by, version, channel, scope, agent_id, status)\n VALUES ($1, $2, $3, $4, 'agent', $5, $6)\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid", + "Uuid", + "Text", + "Text", + "Uuid", + "Text" + ] + }, + "nullable": [] + }, + "hash": "50f8e32fc7290f7c69685700e89ea70d58725b3761a08ce1e26e5a5afca6549d" +} diff --git a/lynx/dashboard/server/.sqlx/query-5102fd11d51afd502b1490a7ab201325a108b99b20f00f325c5f578fe166d47a.json b/lynx/dashboard/server/.sqlx/query-5102fd11d51afd502b1490a7ab201325a108b99b20f00f325c5f578fe166d47a.json new file mode 100644 index 0000000..d326149 --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-5102fd11d51afd502b1490a7ab201325a108b99b20f00f325c5f578fe166d47a.json @@ -0,0 +1,83 @@ +{ + "db_name": "PostgreSQL", + "query": "\n INSERT INTO agents (id, name, wg_pubkey, wg_ip, wg_endpoint, api_port, sync_token_hash, is_local_agent)\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8)\n RETURNING id, name, wg_pubkey, wg_ip, wg_endpoint,\n api_port, status, version, last_heartbeat, created_at\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "name", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "wg_pubkey", + "type_info": "Text" + }, + { + "ordinal": 3, + "name": "wg_ip", + "type_info": "Text" + }, + { + "ordinal": 4, + "name": "wg_endpoint", + "type_info": "Text" + }, + { + "ordinal": 5, + "name": "api_port", + "type_info": "Int4" + }, + { + "ordinal": 6, + "name": "status", + "type_info": "Text" + }, + { + "ordinal": 7, + "name": "version", + "type_info": "Text" + }, + { + "ordinal": 8, + "name": "last_heartbeat", + "type_info": "Timestamptz" + }, + { + "ordinal": 9, + "name": "created_at", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [ + "Uuid", + "Text", + "Text", + "Text", + "Text", + "Int4", + "Text", + "Bool" + ] + }, + "nullable": [ + false, + false, + false, + false, + true, + false, + false, + true, + true, + false + ] + }, + "hash": "5102fd11d51afd502b1490a7ab201325a108b99b20f00f325c5f578fe166d47a" +} diff --git a/lynx/dashboard/server/.sqlx/query-53a2a9841fa11f694257194949b4ab849dacc55f62e7a64a0de22d97674d1fe4.json b/lynx/dashboard/server/.sqlx/query-53a2a9841fa11f694257194949b4ab849dacc55f62e7a64a0de22d97674d1fe4.json new file mode 100644 index 0000000..8a0e1d6 --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-53a2a9841fa11f694257194949b4ab849dacc55f62e7a64a0de22d97674d1fe4.json @@ -0,0 +1,12 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE migration_state SET status='notifying_agents', updated_at=NOW() WHERE id=1", + "describe": { + "columns": [], + "parameters": { + "Left": [] + }, + "nullable": [] + }, + "hash": "53a2a9841fa11f694257194949b4ab849dacc55f62e7a64a0de22d97674d1fe4" +} diff --git a/lynx/dashboard/server/.sqlx/query-563588298c7a6d7d8c6ba0f81b2ca1757d55a7cc67577fd147527e9a0b036bb0.json b/lynx/dashboard/server/.sqlx/query-563588298c7a6d7d8c6ba0f81b2ca1757d55a7cc67577fd147527e9a0b036bb0.json new file mode 100644 index 0000000..e156dfd --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-563588298c7a6d7d8c6ba0f81b2ca1757d55a7cc67577fd147527e9a0b036bb0.json @@ -0,0 +1,15 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE domain_config\n SET cert_type=$1, cert_expires_at=$2, status='active', error_message=NULL, updated_at=NOW()\n WHERE id=1", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Text", + "Timestamptz" + ] + }, + "nullable": [] + }, + "hash": "563588298c7a6d7d8c6ba0f81b2ca1757d55a7cc67577fd147527e9a0b036bb0" +} diff --git a/lynx/dashboard/server/.sqlx/query-573429710cfa33d134397c63dacbf3ab2fdb42cd3b5ad94337c294f61e520414.json b/lynx/dashboard/server/.sqlx/query-573429710cfa33d134397c63dacbf3ab2fdb42cd3b5ad94337c294f61e520414.json new file mode 100644 index 0000000..183eb9e --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-573429710cfa33d134397c63dacbf3ab2fdb42cd3b5ad94337c294f61e520414.json @@ -0,0 +1,15 @@ +{ + "db_name": "PostgreSQL", + "query": "\n INSERT INTO global_rule_sync (rule_id, agent_id)\n VALUES ($1, $2)\n ON CONFLICT (rule_id, agent_id) DO NOTHING\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid", + "Uuid" + ] + }, + "nullable": [] + }, + "hash": "573429710cfa33d134397c63dacbf3ab2fdb42cd3b5ad94337c294f61e520414" +} diff --git a/lynx/dashboard/server/.sqlx/query-576a99e6b7c305f1396f0944664795d79be3e4d6e4606adf44e17a4d4144e3a4.json b/lynx/dashboard/server/.sqlx/query-576a99e6b7c305f1396f0944664795d79be3e4d6e4606adf44e17a4d4144e3a4.json new file mode 100644 index 0000000..c143136 --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-576a99e6b7c305f1396f0944664795d79be3e4d6e4606adf44e17a4d4144e3a4.json @@ -0,0 +1,16 @@ +{ + "db_name": "PostgreSQL", + "query": "\n INSERT INTO organization_members (organization_id, user_id, role)\n VALUES ($1, $2, $3)\n ON CONFLICT (organization_id, user_id) DO NOTHING\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid", + "Uuid", + "Text" + ] + }, + "nullable": [] + }, + "hash": "576a99e6b7c305f1396f0944664795d79be3e4d6e4606adf44e17a4d4144e3a4" +} diff --git a/lynx/dashboard/server/.sqlx/query-586ba988eafbb6a0302acf07c795ba005b1061898fbbbf7adeadad8698928c7a.json b/lynx/dashboard/server/.sqlx/query-586ba988eafbb6a0302acf07c795ba005b1061898fbbbf7adeadad8698928c7a.json new file mode 100644 index 0000000..aa07a66 --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-586ba988eafbb6a0302acf07c795ba005b1061898fbbbf7adeadad8698928c7a.json @@ -0,0 +1,52 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT id, organization_id, agent_id, name, slug, created_at FROM projects WHERE organization_id = $1 ORDER BY created_at ASC", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "organization_id", + "type_info": "Uuid" + }, + { + "ordinal": 2, + "name": "agent_id", + "type_info": "Uuid" + }, + { + "ordinal": 3, + "name": "name", + "type_info": "Text" + }, + { + "ordinal": 4, + "name": "slug", + "type_info": "Text" + }, + { + "ordinal": 5, + "name": "created_at", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [ + false, + false, + false, + false, + false, + false + ] + }, + "hash": "586ba988eafbb6a0302acf07c795ba005b1061898fbbbf7adeadad8698928c7a" +} diff --git a/lynx/dashboard/server/.sqlx/query-59d886170505494a62c65c7b089fc6c8b0af5d39ef14acf7e219e2b9773c0447.json b/lynx/dashboard/server/.sqlx/query-59d886170505494a62c65c7b089fc6c8b0af5d39ef14acf7e219e2b9773c0447.json new file mode 100644 index 0000000..88a5b11 --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-59d886170505494a62c65c7b089fc6c8b0af5d39ef14acf7e219e2b9773c0447.json @@ -0,0 +1,15 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE users SET password_hash = $1, force_password_change = TRUE WHERE id = $2", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Text", + "Uuid" + ] + }, + "nullable": [] + }, + "hash": "59d886170505494a62c65c7b089fc6c8b0af5d39ef14acf7e219e2b9773c0447" +} diff --git a/lynx/dashboard/server/.sqlx/query-5c8172cc810f6fc92205f9b7f5b13995ddf99b29ad0ccae2a36f73638c9934b1.json b/lynx/dashboard/server/.sqlx/query-5c8172cc810f6fc92205f9b7f5b13995ddf99b29ad0ccae2a36f73638c9934b1.json new file mode 100644 index 0000000..403ac53 --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-5c8172cc810f6fc92205f9b7f5b13995ddf99b29ad0ccae2a36f73638c9934b1.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT EXISTS(\n SELECT 1 FROM role_permissions rp\n JOIN permissions p ON p.id = rp.permission_id\n WHERE rp.role_id = $1 AND p.key = '*:*'\n ) AS \"exists!\"", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "exists!", + "type_info": "Bool" + } + ], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [ + null + ] + }, + "hash": "5c8172cc810f6fc92205f9b7f5b13995ddf99b29ad0ccae2a36f73638c9934b1" +} diff --git a/lynx/dashboard/server/.sqlx/query-5de6638fe1feb865f6b7c8d99f9d154aec690ddbd9d716d26c5026c73e93b22e.json b/lynx/dashboard/server/.sqlx/query-5de6638fe1feb865f6b7c8d99f9d154aec690ddbd9d716d26c5026c73e93b22e.json new file mode 100644 index 0000000..8da54bb --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-5de6638fe1feb865f6b7c8d99f9d154aec690ddbd9d716d26c5026c73e93b22e.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT EXISTS(SELECT 1 FROM users WHERE email_hash = $1)", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "exists", + "type_info": "Bool" + } + ], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [ + null + ] + }, + "hash": "5de6638fe1feb865f6b7c8d99f9d154aec690ddbd9d716d26c5026c73e93b22e" +} diff --git a/lynx/dashboard/server/.sqlx/query-5e45ddba50fd7489ade01fc3aa1371282b126a7f5c8b8656bd59ac78531c429b.json b/lynx/dashboard/server/.sqlx/query-5e45ddba50fd7489ade01fc3aa1371282b126a7f5c8b8656bd59ac78531c429b.json new file mode 100644 index 0000000..9bd33a8 --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-5e45ddba50fd7489ade01fc3aa1371282b126a7f5c8b8656bd59ac78531c429b.json @@ -0,0 +1,16 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO roles (id, name, created_by) VALUES ($1, $2, $3)", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid", + "Text", + "Uuid" + ] + }, + "nullable": [] + }, + "hash": "5e45ddba50fd7489ade01fc3aa1371282b126a7f5c8b8656bd59ac78531c429b" +} diff --git a/lynx/dashboard/server/.sqlx/query-617e651d593a9219f924ec16e6eba91a64a4d2eb269923541d0d7c92a87b2c06.json b/lynx/dashboard/server/.sqlx/query-617e651d593a9219f924ec16e6eba91a64a4d2eb269923541d0d7c92a87b2c06.json new file mode 100644 index 0000000..01ca006 --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-617e651d593a9219f924ec16e6eba91a64a4d2eb269923541d0d7c92a87b2c06.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT COALESCE(arch, 'x86_64') FROM agents WHERE id = $1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "coalesce", + "type_info": "Text" + } + ], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [ + null + ] + }, + "hash": "617e651d593a9219f924ec16e6eba91a64a4d2eb269923541d0d7c92a87b2c06" +} diff --git a/lynx/dashboard/server/.sqlx/query-621e833479e86a16d3c95e9f4bd0b9576c3dc5cad3a61bf572c094535dfb248d.json b/lynx/dashboard/server/.sqlx/query-621e833479e86a16d3c95e9f4bd0b9576c3dc5cad3a61bf572c094535dfb248d.json new file mode 100644 index 0000000..006fa8f --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-621e833479e86a16d3c95e9f4bd0b9576c3dc5cad3a61bf572c094535dfb248d.json @@ -0,0 +1,16 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE agents SET status=$1, last_heartbeat=NOW(), version=$2 WHERE id=$3", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Text", + "Text", + "Uuid" + ] + }, + "nullable": [] + }, + "hash": "621e833479e86a16d3c95e9f4bd0b9576c3dc5cad3a61bf572c094535dfb248d" +} diff --git a/lynx/dashboard/server/.sqlx/query-622a1ace62157c5a964a283b5bd82259c595eeef4e3a6acd2a6699fc448d4c8a.json b/lynx/dashboard/server/.sqlx/query-622a1ace62157c5a964a283b5bd82259c595eeef4e3a6acd2a6699fc448d4c8a.json new file mode 100644 index 0000000..dd00180 --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-622a1ace62157c5a964a283b5bd82259c595eeef4e3a6acd2a6699fc448d4c8a.json @@ -0,0 +1,20 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT migration_token_hash FROM migration_state WHERE id=1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "migration_token_hash", + "type_info": "Text" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + true + ] + }, + "hash": "622a1ace62157c5a964a283b5bd82259c595eeef4e3a6acd2a6699fc448d4c8a" +} diff --git a/lynx/dashboard/server/.sqlx/query-639c7dd675a1e706426bed2d129eef64d0a88e5a7e3a0c3f5a34ff6c6ac689a7.json b/lynx/dashboard/server/.sqlx/query-639c7dd675a1e706426bed2d129eef64d0a88e5a7e3a0c3f5a34ff6c6ac689a7.json new file mode 100644 index 0000000..e904d6a --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-639c7dd675a1e706426bed2d129eef64d0a88e5a7e3a0c3f5a34ff6c6ac689a7.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT sync_token_hash FROM agents WHERE id = $1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "sync_token_hash", + "type_info": "Text" + } + ], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [ + true + ] + }, + "hash": "639c7dd675a1e706426bed2d129eef64d0a88e5a7e3a0c3f5a34ff6c6ac689a7" +} diff --git a/lynx/dashboard/server/.sqlx/query-64b8c83f2abaf52c976abf08bae528199de49cb5ef2b84a035b16059b73a315e.json b/lynx/dashboard/server/.sqlx/query-64b8c83f2abaf52c976abf08bae528199de49cb5ef2b84a035b16059b73a315e.json new file mode 100644 index 0000000..adab702 --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-64b8c83f2abaf52c976abf08bae528199de49cb5ef2b84a035b16059b73a315e.json @@ -0,0 +1,12 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE domain_config SET status='active', cert_type='lets_encrypt', cert_expires_at=NOW() + INTERVAL '90 days', updated_at=NOW() WHERE id=1", + "describe": { + "columns": [], + "parameters": { + "Left": [] + }, + "nullable": [] + }, + "hash": "64b8c83f2abaf52c976abf08bae528199de49cb5ef2b84a035b16059b73a315e" +} diff --git a/lynx/dashboard/server/.sqlx/query-64b9345af9f35211fd28e6ca77ffa6eff7e9153696b52f9e4e930bd012df3a5f.json b/lynx/dashboard/server/.sqlx/query-64b9345af9f35211fd28e6ca77ffa6eff7e9153696b52f9e4e930bd012df3a5f.json new file mode 100644 index 0000000..5539875 --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-64b9345af9f35211fd28e6ca77ffa6eff7e9153696b52f9e4e930bd012df3a5f.json @@ -0,0 +1,12 @@ +{ + "db_name": "PostgreSQL", + "query": "DELETE FROM sessions WHERE expires_at > NOW()", + "describe": { + "columns": [], + "parameters": { + "Left": [] + }, + "nullable": [] + }, + "hash": "64b9345af9f35211fd28e6ca77ffa6eff7e9153696b52f9e4e930bd012df3a5f" +} diff --git a/lynx/dashboard/server/.sqlx/query-65f5f75262df10e25cbf75173662cde46a4e0ba48bc341ef0ab1938506850d9d.json b/lynx/dashboard/server/.sqlx/query-65f5f75262df10e25cbf75173662cde46a4e0ba48bc341ef0ab1938506850d9d.json new file mode 100644 index 0000000..7999939 --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-65f5f75262df10e25cbf75173662cde46a4e0ba48bc341ef0ab1938506850d9d.json @@ -0,0 +1,15 @@ +{ + "db_name": "PostgreSQL", + "query": "DELETE FROM user_roles WHERE user_id = $1 AND role_id = $2", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid", + "Uuid" + ] + }, + "nullable": [] + }, + "hash": "65f5f75262df10e25cbf75173662cde46a4e0ba48bc341ef0ab1938506850d9d" +} diff --git a/lynx/dashboard/server/.sqlx/query-6627a3626d64ddf45ea6057054f6964dd9c2f7b874f8dcc8f81a4bca699b294c.json b/lynx/dashboard/server/.sqlx/query-6627a3626d64ddf45ea6057054f6964dd9c2f7b874f8dcc8f81a4bca699b294c.json new file mode 100644 index 0000000..5ac404e --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-6627a3626d64ddf45ea6057054f6964dd9c2f7b874f8dcc8f81a4bca699b294c.json @@ -0,0 +1,40 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT id, user_id, refresh_token_hash, expires_at\n FROM sessions\n WHERE refresh_token_hash = $1\n AND expires_at > NOW()\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "user_id", + "type_info": "Uuid" + }, + { + "ordinal": 2, + "name": "refresh_token_hash", + "type_info": "Text" + }, + { + "ordinal": 3, + "name": "expires_at", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [ + false, + false, + false, + false + ] + }, + "hash": "6627a3626d64ddf45ea6057054f6964dd9c2f7b874f8dcc8f81a4bca699b294c" +} diff --git a/lynx/dashboard/server/.sqlx/query-663d3a4b8d35369a26a87babe28e095cd58598ec1fe4bd737b6d97420ff0595b.json b/lynx/dashboard/server/.sqlx/query-663d3a4b8d35369a26a87babe28e095cd58598ec1fe4bd737b6d97420ff0595b.json new file mode 100644 index 0000000..375207c --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-663d3a4b8d35369a26a87babe28e095cd58598ec1fe4bd737b6d97420ff0595b.json @@ -0,0 +1,15 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE agents SET status=$1, last_heartbeat=NOW() WHERE id=$2", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Text", + "Uuid" + ] + }, + "nullable": [] + }, + "hash": "663d3a4b8d35369a26a87babe28e095cd58598ec1fe4bd737b6d97420ff0595b" +} diff --git a/lynx/dashboard/server/.sqlx/query-68afa06b76ee08c71260c00d81b094726dd1ad7f0c7b4640ea511b31b0ceabd7.json b/lynx/dashboard/server/.sqlx/query-68afa06b76ee08c71260c00d81b094726dd1ad7f0c7b4640ea511b31b0ceabd7.json new file mode 100644 index 0000000..c324151 --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-68afa06b76ee08c71260c00d81b094726dd1ad7f0c7b4640ea511b31b0ceabd7.json @@ -0,0 +1,20 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT EXISTS(\n SELECT 1 FROM user_roles ur\n JOIN role_permissions rp ON rp.role_id = ur.role_id\n JOIN permissions p ON p.id = rp.permission_id\n WHERE p.key = '*:*'\n )\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "exists", + "type_info": "Bool" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + null + ] + }, + "hash": "68afa06b76ee08c71260c00d81b094726dd1ad7f0c7b4640ea511b31b0ceabd7" +} diff --git a/lynx/dashboard/server/.sqlx/query-69b6c00fd7ce74da9ed65c826367c3c12ba071ca56465864d84472d8b62a1a5c.json b/lynx/dashboard/server/.sqlx/query-69b6c00fd7ce74da9ed65c826367c3c12ba071ca56465864d84472d8b62a1a5c.json new file mode 100644 index 0000000..df4c83d --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-69b6c00fd7ce74da9ed65c826367c3c12ba071ca56465864d84472d8b62a1a5c.json @@ -0,0 +1,15 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE users SET single_session = $1 WHERE id = $2", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Bool", + "Uuid" + ] + }, + "nullable": [] + }, + "hash": "69b6c00fd7ce74da9ed65c826367c3c12ba071ca56465864d84472d8b62a1a5c" +} diff --git a/lynx/dashboard/server/.sqlx/query-6a69dac17737d8209d8ddf9fef4142a63355320e75a8379e29b6879b8ba93a73.json b/lynx/dashboard/server/.sqlx/query-6a69dac17737d8209d8ddf9fef4142a63355320e75a8379e29b6879b8ba93a73.json new file mode 100644 index 0000000..39c026c --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-6a69dac17737d8209d8ddf9fef4142a63355320e75a8379e29b6879b8ba93a73.json @@ -0,0 +1,12 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE migration_state SET agents_confirmed = agents_confirmed + 1, updated_at=NOW() WHERE id=1", + "describe": { + "columns": [], + "parameters": { + "Left": [] + }, + "nullable": [] + }, + "hash": "6a69dac17737d8209d8ddf9fef4142a63355320e75a8379e29b6879b8ba93a73" +} diff --git a/lynx/dashboard/server/.sqlx/query-6b359c6243c482867b6fc4f7e2e3086f797a6452b27c7c9ce2eef6f1d4932fbd.json b/lynx/dashboard/server/.sqlx/query-6b359c6243c482867b6fc4f7e2e3086f797a6452b27c7c9ce2eef6f1d4932fbd.json new file mode 100644 index 0000000..b605b31 --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-6b359c6243c482867b6fc4f7e2e3086f797a6452b27c7c9ce2eef6f1d4932fbd.json @@ -0,0 +1,20 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT status FROM domain_config WHERE id = 1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "status", + "type_info": "Text" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + false + ] + }, + "hash": "6b359c6243c482867b6fc4f7e2e3086f797a6452b27c7c9ce2eef6f1d4932fbd" +} diff --git a/lynx/dashboard/server/.sqlx/query-6b96b6b09dd833489fd9042ea7b486e8da49b3263e2051b9c5df7f9d134e25dd.json b/lynx/dashboard/server/.sqlx/query-6b96b6b09dd833489fd9042ea7b486e8da49b3263e2051b9c5df7f9d134e25dd.json new file mode 100644 index 0000000..2b0c526 --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-6b96b6b09dd833489fd9042ea7b486e8da49b3263e2051b9c5df7f9d134e25dd.json @@ -0,0 +1,15 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE ip_pool SET agent_id = $1, updated_at = NOW() WHERE ip::text = $2", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid", + "Text" + ] + }, + "nullable": [] + }, + "hash": "6b96b6b09dd833489fd9042ea7b486e8da49b3263e2051b9c5df7f9d134e25dd" +} diff --git a/lynx/dashboard/server/.sqlx/query-6d5dc21c6a9e2864fca14e3babe32f70a23626f102a4055a47d154f0e8c8e52a.json b/lynx/dashboard/server/.sqlx/query-6d5dc21c6a9e2864fca14e3babe32f70a23626f102a4055a47d154f0e8c8e52a.json new file mode 100644 index 0000000..cb8d5dd --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-6d5dc21c6a9e2864fca14e3babe32f70a23626f102a4055a47d154f0e8c8e52a.json @@ -0,0 +1,12 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE users SET force_password_change = TRUE", + "describe": { + "columns": [], + "parameters": { + "Left": [] + }, + "nullable": [] + }, + "hash": "6d5dc21c6a9e2864fca14e3babe32f70a23626f102a4055a47d154f0e8c8e52a" +} diff --git a/lynx/dashboard/server/.sqlx/query-70cc31c3bd3d6543324220b870f7705b3e6b4886fc4ef18b0b020d90cfdc5fb3.json b/lynx/dashboard/server/.sqlx/query-70cc31c3bd3d6543324220b870f7705b3e6b4886fc4ef18b0b020d90cfdc5fb3.json new file mode 100644 index 0000000..e01a32b --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-70cc31c3bd3d6543324220b870f7705b3e6b4886fc4ef18b0b020d90cfdc5fb3.json @@ -0,0 +1,40 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT m.user_id, u.username, m.role, m.joined_at\n FROM organization_members m\n JOIN users u ON u.id = m.user_id\n WHERE m.organization_id = $1\n ORDER BY m.joined_at ASC\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "user_id", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "username", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "role", + "type_info": "Text" + }, + { + "ordinal": 3, + "name": "joined_at", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [ + false, + false, + false, + false + ] + }, + "hash": "70cc31c3bd3d6543324220b870f7705b3e6b4886fc4ef18b0b020d90cfdc5fb3" +} diff --git a/lynx/dashboard/server/.sqlx/query-70de9a54908258f9ffff93ba42efdb5407b9a9ed9425098d0c2544b6541a4d7e.json b/lynx/dashboard/server/.sqlx/query-70de9a54908258f9ffff93ba42efdb5407b9a9ed9425098d0c2544b6541a4d7e.json new file mode 100644 index 0000000..243f882 --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-70de9a54908258f9ffff93ba42efdb5407b9a9ed9425098d0c2544b6541a4d7e.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT COUNT(DISTINCT ur.user_id) FROM user_roles ur\n JOIN role_permissions rp ON rp.role_id = ur.role_id\n JOIN permissions p ON p.id = rp.permission_id\n WHERE ur.role_id = $1 AND p.key = '*:*'\n AND NOT EXISTS (\n SELECT 1 FROM user_roles ur2\n JOIN role_permissions rp2 ON rp2.role_id = ur2.role_id\n JOIN permissions p2 ON p2.id = rp2.permission_id\n WHERE ur2.user_id = ur.user_id AND ur2.role_id != $1 AND p2.key = '*:*'\n )", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "count", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [ + null + ] + }, + "hash": "70de9a54908258f9ffff93ba42efdb5407b9a9ed9425098d0c2544b6541a4d7e" +} diff --git a/lynx/dashboard/server/.sqlx/query-712fdd722044311a97343c524de4bfbf8375cee56d14643670072b670b347195.json b/lynx/dashboard/server/.sqlx/query-712fdd722044311a97343c524de4bfbf8375cee56d14643670072b670b347195.json new file mode 100644 index 0000000..8046594 --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-712fdd722044311a97343c524de4bfbf8375cee56d14643670072b670b347195.json @@ -0,0 +1,17 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO security_alerts (id, kind, detail, agent_id) VALUES ($1, $2, $3, $4)", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid", + "Text", + "Text", + "Uuid" + ] + }, + "nullable": [] + }, + "hash": "712fdd722044311a97343c524de4bfbf8375cee56d14643670072b670b347195" +} diff --git a/lynx/dashboard/server/.sqlx/query-73ac44f1c18b35583637cc51a439f147feacfcbe765c69a63d20621cce25fb8a.json b/lynx/dashboard/server/.sqlx/query-73ac44f1c18b35583637cc51a439f147feacfcbe765c69a63d20621cce25fb8a.json new file mode 100644 index 0000000..20e9a07 --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-73ac44f1c18b35583637cc51a439f147feacfcbe765c69a63d20621cce25fb8a.json @@ -0,0 +1,40 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT id, password_hash, force_password_change, single_session FROM users WHERE username = $1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "password_hash", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "force_password_change", + "type_info": "Bool" + }, + { + "ordinal": 3, + "name": "single_session", + "type_info": "Bool" + } + ], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [ + false, + false, + false, + false + ] + }, + "hash": "73ac44f1c18b35583637cc51a439f147feacfcbe765c69a63d20621cce25fb8a" +} diff --git a/lynx/dashboard/server/.sqlx/query-76269ec9536854980b4274646932c8dc6eb760fbcbe636596dbaa7e8963dbed3.json b/lynx/dashboard/server/.sqlx/query-76269ec9536854980b4274646932c8dc6eb760fbcbe636596dbaa7e8963dbed3.json new file mode 100644 index 0000000..d8b7773 --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-76269ec9536854980b4274646932c8dc6eb760fbcbe636596dbaa7e8963dbed3.json @@ -0,0 +1,15 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO agent_events (id, agent_id, event, detail) VALUES ($1, $2, 'heartbeat_lost', NULL)", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid", + "Uuid" + ] + }, + "nullable": [] + }, + "hash": "76269ec9536854980b4274646932c8dc6eb760fbcbe636596dbaa7e8963dbed3" +} diff --git a/lynx/dashboard/server/.sqlx/query-76a7e92c144ac7ff3992987838d894bd58d2bf0e4f61101192fece85284d40ff.json b/lynx/dashboard/server/.sqlx/query-76a7e92c144ac7ff3992987838d894bd58d2bf0e4f61101192fece85284d40ff.json new file mode 100644 index 0000000..fa89f1d --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-76a7e92c144ac7ff3992987838d894bd58d2bf0e4f61101192fece85284d40ff.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT EXISTS(SELECT 1 FROM users WHERE id = $1) AS \"exists!\"", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "exists!", + "type_info": "Bool" + } + ], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [ + null + ] + }, + "hash": "76a7e92c144ac7ff3992987838d894bd58d2bf0e4f61101192fece85284d40ff" +} diff --git a/lynx/dashboard/server/.sqlx/query-772e7f05553ab4e646c67af8984fa9e624ef440f2c552997e859c3093d973961.json b/lynx/dashboard/server/.sqlx/query-772e7f05553ab4e646c67af8984fa9e624ef440f2c552997e859c3093d973961.json new file mode 100644 index 0000000..acedea9 --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-772e7f05553ab4e646c67af8984fa9e624ef440f2c552997e859c3093d973961.json @@ -0,0 +1,53 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT id, organization_id, agent_id, name, slug, created_at FROM projects WHERE id = $1 AND organization_id = $2", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "organization_id", + "type_info": "Uuid" + }, + { + "ordinal": 2, + "name": "agent_id", + "type_info": "Uuid" + }, + { + "ordinal": 3, + "name": "name", + "type_info": "Text" + }, + { + "ordinal": 4, + "name": "slug", + "type_info": "Text" + }, + { + "ordinal": 5, + "name": "created_at", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [ + "Uuid", + "Uuid" + ] + }, + "nullable": [ + false, + false, + false, + false, + false, + false + ] + }, + "hash": "772e7f05553ab4e646c67af8984fa9e624ef440f2c552997e859c3093d973961" +} diff --git a/lynx/dashboard/server/.sqlx/query-79d72babe1eda061141c45a0b860ab42825a304ef823555fd61db303c8a852ba.json b/lynx/dashboard/server/.sqlx/query-79d72babe1eda061141c45a0b860ab42825a304ef823555fd61db303c8a852ba.json new file mode 100644 index 0000000..e28a56b --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-79d72babe1eda061141c45a0b860ab42825a304ef823555fd61db303c8a852ba.json @@ -0,0 +1,23 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT 1 FROM projects WHERE id = $1 AND organization_id = $2", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "?column?", + "type_info": "Int4" + } + ], + "parameters": { + "Left": [ + "Uuid", + "Uuid" + ] + }, + "nullable": [ + null + ] + }, + "hash": "79d72babe1eda061141c45a0b860ab42825a304ef823555fd61db303c8a852ba" +} diff --git a/lynx/dashboard/server/.sqlx/query-7a2c43464e88166ffe4e4103dbf1867d853659b5b4fd6d352583a254d0a7a18e.json b/lynx/dashboard/server/.sqlx/query-7a2c43464e88166ffe4e4103dbf1867d853659b5b4fd6d352583a254d0a7a18e.json new file mode 100644 index 0000000..88f31d7 --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-7a2c43464e88166ffe4e4103dbf1867d853659b5b4fd6d352583a254d0a7a18e.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT entry_hash FROM audit_log WHERE agent_id = $1 ORDER BY created_at DESC LIMIT 1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "entry_hash", + "type_info": "Text" + } + ], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [ + false + ] + }, + "hash": "7a2c43464e88166ffe4e4103dbf1867d853659b5b4fd6d352583a254d0a7a18e" +} diff --git a/lynx/dashboard/server/.sqlx/query-7aa9e8d60074416941adc937e4b63890bab9a496d1ae69a3fbb6500910e136f8.json b/lynx/dashboard/server/.sqlx/query-7aa9e8d60074416941adc937e4b63890bab9a496d1ae69a3fbb6500910e136f8.json new file mode 100644 index 0000000..20f1af0 --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-7aa9e8d60074416941adc937e4b63890bab9a496d1ae69a3fbb6500910e136f8.json @@ -0,0 +1,12 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE migration_state SET status='transferring', updated_at=NOW() WHERE id=1", + "describe": { + "columns": [], + "parameters": { + "Left": [] + }, + "nullable": [] + }, + "hash": "7aa9e8d60074416941adc937e4b63890bab9a496d1ae69a3fbb6500910e136f8" +} diff --git a/lynx/dashboard/server/.sqlx/query-7bf3f04bf7a71c6c23c14d30ca81ff89b849ac4235df869801f34f19901b5445.json b/lynx/dashboard/server/.sqlx/query-7bf3f04bf7a71c6c23c14d30ca81ff89b849ac4235df869801f34f19901b5445.json new file mode 100644 index 0000000..a7b00ce --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-7bf3f04bf7a71c6c23c14d30ca81ff89b849ac4235df869801f34f19901b5445.json @@ -0,0 +1,74 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT id, status, role, target_url, agents_total, agents_confirmed,\n error_message, started_at, completed_at, updated_at\n FROM migration_state WHERE id = 1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int4" + }, + { + "ordinal": 1, + "name": "status", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "role", + "type_info": "Text" + }, + { + "ordinal": 3, + "name": "target_url", + "type_info": "Text" + }, + { + "ordinal": 4, + "name": "agents_total", + "type_info": "Int4" + }, + { + "ordinal": 5, + "name": "agents_confirmed", + "type_info": "Int4" + }, + { + "ordinal": 6, + "name": "error_message", + "type_info": "Text" + }, + { + "ordinal": 7, + "name": "started_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 8, + "name": "completed_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 9, + "name": "updated_at", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + false, + false, + false, + true, + false, + false, + true, + true, + true, + false + ] + }, + "hash": "7bf3f04bf7a71c6c23c14d30ca81ff89b849ac4235df869801f34f19901b5445" +} diff --git a/lynx/dashboard/server/.sqlx/query-825215844cc17aede2cff3845f2ac2bfe0a301005821fb018c60f3dda7c5f0e0.json b/lynx/dashboard/server/.sqlx/query-825215844cc17aede2cff3845f2ac2bfe0a301005821fb018c60f3dda7c5f0e0.json new file mode 100644 index 0000000..14d4ec6 --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-825215844cc17aede2cff3845f2ac2bfe0a301005821fb018c60f3dda7c5f0e0.json @@ -0,0 +1,28 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT id, last_jti FROM sessions WHERE user_id = $1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "last_jti", + "type_info": "Uuid" + } + ], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [ + false, + true + ] + }, + "hash": "825215844cc17aede2cff3845f2ac2bfe0a301005821fb018c60f3dda7c5f0e0" +} diff --git a/lynx/dashboard/server/.sqlx/query-897e779d71e87a660237ebe5c1bd4bd8861eca3c4451618be43b579f134f557d.json b/lynx/dashboard/server/.sqlx/query-897e779d71e87a660237ebe5c1bd4bd8861eca3c4451618be43b579f134f557d.json new file mode 100644 index 0000000..99bfde8 --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-897e779d71e87a660237ebe5c1bd4bd8861eca3c4451618be43b579f134f557d.json @@ -0,0 +1,23 @@ +{ + "db_name": "PostgreSQL", + "query": "DELETE FROM sessions WHERE id = $1 AND user_id = $2 RETURNING last_jti", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "last_jti", + "type_info": "Uuid" + } + ], + "parameters": { + "Left": [ + "Uuid", + "Uuid" + ] + }, + "nullable": [ + true + ] + }, + "hash": "897e779d71e87a660237ebe5c1bd4bd8861eca3c4451618be43b579f134f557d" +} diff --git a/lynx/dashboard/server/.sqlx/query-8bd96aa5c97181d1dbe44ea92d19eedaea4bdbdbc8a974b289992eb6885f9007.json b/lynx/dashboard/server/.sqlx/query-8bd96aa5c97181d1dbe44ea92d19eedaea4bdbdbc8a974b289992eb6885f9007.json new file mode 100644 index 0000000..d789999 --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-8bd96aa5c97181d1dbe44ea92d19eedaea4bdbdbc8a974b289992eb6885f9007.json @@ -0,0 +1,17 @@ +{ + "db_name": "PostgreSQL", + "query": "\n INSERT INTO rotation_log (id, triggered_by, reason, scope)\n VALUES ($1, $2, $3, $4)\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid", + "Uuid", + "Text", + "Text" + ] + }, + "nullable": [] + }, + "hash": "8bd96aa5c97181d1dbe44ea92d19eedaea4bdbdbc8a974b289992eb6885f9007" +} diff --git a/lynx/dashboard/server/.sqlx/query-8fcd12f6016d9fd33fd10bb3337858feaaf63c7f83fa8a3a8abd2b3e57f35535.json b/lynx/dashboard/server/.sqlx/query-8fcd12f6016d9fd33fd10bb3337858feaaf63c7f83fa8a3a8abd2b3e57f35535.json new file mode 100644 index 0000000..010ae64 --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-8fcd12f6016d9fd33fd10bb3337858feaaf63c7f83fa8a3a8abd2b3e57f35535.json @@ -0,0 +1,17 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO role_permissions (id, role_id, permission_id, created_by) VALUES ($1, $2, $3, $4)\n ON CONFLICT (role_id, permission_id) DO NOTHING", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid", + "Uuid", + "Uuid", + "Uuid" + ] + }, + "nullable": [] + }, + "hash": "8fcd12f6016d9fd33fd10bb3337858feaaf63c7f83fa8a3a8abd2b3e57f35535" +} diff --git a/lynx/dashboard/server/.sqlx/query-91aa2e4739161f9f8786c84877b18aabeef08fedd2c9f78586e654e35d7ea778.json b/lynx/dashboard/server/.sqlx/query-91aa2e4739161f9f8786c84877b18aabeef08fedd2c9f78586e654e35d7ea778.json new file mode 100644 index 0000000..4170031 --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-91aa2e4739161f9f8786c84877b18aabeef08fedd2c9f78586e654e35d7ea778.json @@ -0,0 +1,50 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT company_name, logo_url, primary_color, secondary_color, accent_color, updated_at FROM white_label WHERE id = 1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "company_name", + "type_info": "Text" + }, + { + "ordinal": 1, + "name": "logo_url", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "primary_color", + "type_info": "Text" + }, + { + "ordinal": 3, + "name": "secondary_color", + "type_info": "Text" + }, + { + "ordinal": 4, + "name": "accent_color", + "type_info": "Text" + }, + { + "ordinal": 5, + "name": "updated_at", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + false, + true, + false, + false, + false, + false + ] + }, + "hash": "91aa2e4739161f9f8786c84877b18aabeef08fedd2c9f78586e654e35d7ea778" +} diff --git a/lynx/dashboard/server/.sqlx/query-924b5f89e41e330bb578e2c4cdac1157ae4b785fe9fd5313a842188e33f9be31.json b/lynx/dashboard/server/.sqlx/query-924b5f89e41e330bb578e2c4cdac1157ae4b785fe9fd5313a842188e33f9be31.json new file mode 100644 index 0000000..2da341b --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-924b5f89e41e330bb578e2c4cdac1157ae4b785fe9fd5313a842188e33f9be31.json @@ -0,0 +1,68 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT id, triggered_by, version, channel, scope, agent_id, status, error, created_at\n FROM update_log\n ORDER BY created_at DESC\n LIMIT 50\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "triggered_by", + "type_info": "Uuid" + }, + { + "ordinal": 2, + "name": "version", + "type_info": "Text" + }, + { + "ordinal": 3, + "name": "channel", + "type_info": "Text" + }, + { + "ordinal": 4, + "name": "scope", + "type_info": "Text" + }, + { + "ordinal": 5, + "name": "agent_id", + "type_info": "Uuid" + }, + { + "ordinal": 6, + "name": "status", + "type_info": "Text" + }, + { + "ordinal": 7, + "name": "error", + "type_info": "Text" + }, + { + "ordinal": 8, + "name": "created_at", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + false, + true, + false, + false, + false, + true, + false, + true, + false + ] + }, + "hash": "924b5f89e41e330bb578e2c4cdac1157ae4b785fe9fd5313a842188e33f9be31" +} diff --git a/lynx/dashboard/server/.sqlx/query-9278b37726d138938ca9c9ea2b55e270aede83768d10e93033a360c877493589.json b/lynx/dashboard/server/.sqlx/query-9278b37726d138938ca9c9ea2b55e270aede83768d10e93033a360c877493589.json new file mode 100644 index 0000000..376c29a --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-9278b37726d138938ca9c9ea2b55e270aede83768d10e93033a360c877493589.json @@ -0,0 +1,26 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT agents_total, agents_confirmed FROM migration_state WHERE id=1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "agents_total", + "type_info": "Int4" + }, + { + "ordinal": 1, + "name": "agents_confirmed", + "type_info": "Int4" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + false, + false + ] + }, + "hash": "9278b37726d138938ca9c9ea2b55e270aede83768d10e93033a360c877493589" +} diff --git a/lynx/dashboard/server/.sqlx/query-92aa51b8f292d7a7ad739b53adba78adff789460ab2fef1a7a47731cebd37a2b.json b/lynx/dashboard/server/.sqlx/query-92aa51b8f292d7a7ad739b53adba78adff789460ab2fef1a7a47731cebd37a2b.json new file mode 100644 index 0000000..ee69b69 --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-92aa51b8f292d7a7ad739b53adba78adff789460ab2fef1a7a47731cebd37a2b.json @@ -0,0 +1,20 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT MAX(created_at) FROM rotation_log WHERE reason IN ('scheduled', 'update')", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "max", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + null + ] + }, + "hash": "92aa51b8f292d7a7ad739b53adba78adff789460ab2fef1a7a47731cebd37a2b" +} diff --git a/lynx/dashboard/server/.sqlx/query-949496a62ce0e92243c088e183afa5ce958a40c62f069ae37d7348bdb607a9d3.json b/lynx/dashboard/server/.sqlx/query-949496a62ce0e92243c088e183afa5ce958a40c62f069ae37d7348bdb607a9d3.json new file mode 100644 index 0000000..66493c6 --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-949496a62ce0e92243c088e183afa5ce958a40c62f069ae37d7348bdb607a9d3.json @@ -0,0 +1,14 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE ip_pool SET agent_id = NULL, updated_at = NOW() WHERE agent_id = $1", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [] + }, + "hash": "949496a62ce0e92243c088e183afa5ce958a40c62f069ae37d7348bdb607a9d3" +} diff --git a/lynx/dashboard/server/.sqlx/query-94b1cb3a0ba7f37ccfc86c83499caa7c5ba238d97731f21cb4f366d676d039e4.json b/lynx/dashboard/server/.sqlx/query-94b1cb3a0ba7f37ccfc86c83499caa7c5ba238d97731f21cb4f366d676d039e4.json new file mode 100644 index 0000000..d83d217 --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-94b1cb3a0ba7f37ccfc86c83499caa7c5ba238d97731f21cb4f366d676d039e4.json @@ -0,0 +1,28 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT wg_ip::text AS wg_ip, api_port FROM agents WHERE id = $1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "wg_ip", + "type_info": "Text" + }, + { + "ordinal": 1, + "name": "api_port", + "type_info": "Int4" + } + ], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [ + false, + false + ] + }, + "hash": "94b1cb3a0ba7f37ccfc86c83499caa7c5ba238d97731f21cb4f366d676d039e4" +} diff --git a/lynx/dashboard/server/.sqlx/query-95c362b52fb98487642ea5875c52eb5d584b9414631dad761007efc610fb16cd.json b/lynx/dashboard/server/.sqlx/query-95c362b52fb98487642ea5875c52eb5d584b9414631dad761007efc610fb16cd.json new file mode 100644 index 0000000..76e018c --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-95c362b52fb98487642ea5875c52eb5d584b9414631dad761007efc610fb16cd.json @@ -0,0 +1,20 @@ +{ + "db_name": "PostgreSQL", + "query": "\n INSERT INTO sessions (id, user_id, ip, user_agent, refresh_token_hash, expires_at, last_jti)\n VALUES ($1, $2, $3, $4, $5, $6, $7)\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid", + "Uuid", + "Text", + "Text", + "Text", + "Timestamptz", + "Uuid" + ] + }, + "nullable": [] + }, + "hash": "95c362b52fb98487642ea5875c52eb5d584b9414631dad761007efc610fb16cd" +} diff --git a/lynx/dashboard/server/.sqlx/query-976b52de19f415be5fbbf5b025df2668dbc489dcf6f20442d4f8ef635977c6d6.json b/lynx/dashboard/server/.sqlx/query-976b52de19f415be5fbbf5b025df2668dbc489dcf6f20442d4f8ef635977c6d6.json new file mode 100644 index 0000000..8fe79dc --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-976b52de19f415be5fbbf5b025df2668dbc489dcf6f20442d4f8ef635977c6d6.json @@ -0,0 +1,20 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT COUNT(*) FROM agents", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "count", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + null + ] + }, + "hash": "976b52de19f415be5fbbf5b025df2668dbc489dcf6f20442d4f8ef635977c6d6" +} diff --git a/lynx/dashboard/server/.sqlx/query-977b4f94a6a1951a48c3526187a598d18f2c3da9c280c151a9786cd3ae0c18b6.json b/lynx/dashboard/server/.sqlx/query-977b4f94a6a1951a48c3526187a598d18f2c3da9c280c151a9786cd3ae0c18b6.json new file mode 100644 index 0000000..316f6aa --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-977b4f94a6a1951a48c3526187a598d18f2c3da9c280c151a9786cd3ae0c18b6.json @@ -0,0 +1,35 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT agent_a_id, agent_b_id, replica_count FROM data_plane_tunnels WHERE id=$1 AND project_id=$2", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "agent_a_id", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "agent_b_id", + "type_info": "Uuid" + }, + { + "ordinal": 2, + "name": "replica_count", + "type_info": "Int4" + } + ], + "parameters": { + "Left": [ + "Uuid", + "Uuid" + ] + }, + "nullable": [ + false, + false, + false + ] + }, + "hash": "977b4f94a6a1951a48c3526187a598d18f2c3da9c280c151a9786cd3ae0c18b6" +} diff --git a/lynx/dashboard/server/.sqlx/query-9a5b430585bf8c19c0a5915dfd5744d4b33edd500e7027cd051302debc29fa53.json b/lynx/dashboard/server/.sqlx/query-9a5b430585bf8c19c0a5915dfd5744d4b33edd500e7027cd051302debc29fa53.json new file mode 100644 index 0000000..4215ae9 --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-9a5b430585bf8c19c0a5915dfd5744d4b33edd500e7027cd051302debc29fa53.json @@ -0,0 +1,14 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE global_rule_sync SET synced_at = NOW() WHERE agent_id = $1 AND synced_at IS NULL", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [] + }, + "hash": "9a5b430585bf8c19c0a5915dfd5744d4b33edd500e7027cd051302debc29fa53" +} diff --git a/lynx/dashboard/server/.sqlx/query-9ac83fc6f92c179ef552bd00c922cf22eb1665a8248f931d18dcd4dcb9759cb9.json b/lynx/dashboard/server/.sqlx/query-9ac83fc6f92c179ef552bd00c922cf22eb1665a8248f931d18dcd4dcb9759cb9.json new file mode 100644 index 0000000..0faa151 --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-9ac83fc6f92c179ef552bd00c922cf22eb1665a8248f931d18dcd4dcb9759cb9.json @@ -0,0 +1,116 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT id, scope, agent_id, kind, port, port_end, protocol,\n ip_list, ip_version, rate_per_min, description,\n priority, enabled, direction, created_by, created_at, updated_at\n FROM nftables_rules\n WHERE scope = 'global'\n ORDER BY priority ASC, created_at ASC\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "scope", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "agent_id", + "type_info": "Uuid" + }, + { + "ordinal": 3, + "name": "kind", + "type_info": "Text" + }, + { + "ordinal": 4, + "name": "port", + "type_info": "Int4" + }, + { + "ordinal": 5, + "name": "port_end", + "type_info": "Int4" + }, + { + "ordinal": 6, + "name": "protocol", + "type_info": "Text" + }, + { + "ordinal": 7, + "name": "ip_list", + "type_info": "TextArray" + }, + { + "ordinal": 8, + "name": "ip_version", + "type_info": "Text" + }, + { + "ordinal": 9, + "name": "rate_per_min", + "type_info": "Int4" + }, + { + "ordinal": 10, + "name": "description", + "type_info": "Text" + }, + { + "ordinal": 11, + "name": "priority", + "type_info": "Int4" + }, + { + "ordinal": 12, + "name": "enabled", + "type_info": "Bool" + }, + { + "ordinal": 13, + "name": "direction", + "type_info": "Text" + }, + { + "ordinal": 14, + "name": "created_by", + "type_info": "Uuid" + }, + { + "ordinal": 15, + "name": "created_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 16, + "name": "updated_at", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + false, + false, + true, + false, + true, + true, + true, + false, + false, + true, + true, + false, + false, + false, + true, + false, + false + ] + }, + "hash": "9ac83fc6f92c179ef552bd00c922cf22eb1665a8248f931d18dcd4dcb9759cb9" +} diff --git a/lynx/dashboard/server/.sqlx/query-9b2052904dc1f773809600a88eb61bb8164b8013f7e2c4b6c2db15077e554cb1.json b/lynx/dashboard/server/.sqlx/query-9b2052904dc1f773809600a88eb61bb8164b8013f7e2c4b6c2db15077e554cb1.json new file mode 100644 index 0000000..d384b48 --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-9b2052904dc1f773809600a88eb61bb8164b8013f7e2c4b6c2db15077e554cb1.json @@ -0,0 +1,32 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT status, domain, cert_type FROM domain_config WHERE id = 1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "status", + "type_info": "Text" + }, + { + "ordinal": 1, + "name": "domain", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "cert_type", + "type_info": "Text" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + false, + true, + false + ] + }, + "hash": "9b2052904dc1f773809600a88eb61bb8164b8013f7e2c4b6c2db15077e554cb1" +} diff --git a/lynx/dashboard/server/.sqlx/query-9d4f7db0d69ff3e9cbefd6fada39126028db165f45aa6c97ccf0f30d79b6ebf0.json b/lynx/dashboard/server/.sqlx/query-9d4f7db0d69ff3e9cbefd6fada39126028db165f45aa6c97ccf0f30d79b6ebf0.json new file mode 100644 index 0000000..141d089 --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-9d4f7db0d69ff3e9cbefd6fada39126028db165f45aa6c97ccf0f30d79b6ebf0.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT COUNT(DISTINCT ur.user_id) FROM user_roles ur\n JOIN role_permissions rp ON rp.role_id = ur.role_id\n JOIN permissions p ON p.id = rp.permission_id\n WHERE ur.role_id = $1 AND p.key = '*:*'", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "count", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [ + null + ] + }, + "hash": "9d4f7db0d69ff3e9cbefd6fada39126028db165f45aa6c97ccf0f30d79b6ebf0" +} diff --git a/lynx/dashboard/server/.sqlx/query-9fdee5b0e9b25ae3419bf77b8c2d4427dfa8929da5343a0c116dd486828e1faa.json b/lynx/dashboard/server/.sqlx/query-9fdee5b0e9b25ae3419bf77b8c2d4427dfa8929da5343a0c116dd486828e1faa.json new file mode 100644 index 0000000..b860840 --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-9fdee5b0e9b25ae3419bf77b8c2d4427dfa8929da5343a0c116dd486828e1faa.json @@ -0,0 +1,15 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE update_log SET status = $1 WHERE id = $2", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Text", + "Uuid" + ] + }, + "nullable": [] + }, + "hash": "9fdee5b0e9b25ae3419bf77b8c2d4427dfa8929da5343a0c116dd486828e1faa" +} diff --git a/lynx/dashboard/server/.sqlx/query-a101f3e73dc4bac366a836b7ff667cc7347b48abd4464732076646980b5d330a.json b/lynx/dashboard/server/.sqlx/query-a101f3e73dc4bac366a836b7ff667cc7347b48abd4464732076646980b5d330a.json new file mode 100644 index 0000000..f02ce0f --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-a101f3e73dc4bac366a836b7ff667cc7347b48abd4464732076646980b5d330a.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT version FROM agents WHERE id = $1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "version", + "type_info": "Text" + } + ], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [ + true + ] + }, + "hash": "a101f3e73dc4bac366a836b7ff667cc7347b48abd4464732076646980b5d330a" +} diff --git a/lynx/dashboard/server/.sqlx/query-a1cdd2d0e616783625de8cf307a359b8757df8911afaff0358e97b8b7769bf41.json b/lynx/dashboard/server/.sqlx/query-a1cdd2d0e616783625de8cf307a359b8757df8911afaff0358e97b8b7769bf41.json new file mode 100644 index 0000000..b12d97f --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-a1cdd2d0e616783625de8cf307a359b8757df8911afaff0358e97b8b7769bf41.json @@ -0,0 +1,49 @@ +{ + "db_name": "PostgreSQL", + "query": "\n WITH new_org AS (\n INSERT INTO organizations (id, name, slug, owner_id)\n VALUES ($1, $2, $3, $4)\n RETURNING *\n ),\n _ AS (\n INSERT INTO organization_members (organization_id, user_id, role)\n VALUES ($1, $4, 'owner')\n )\n SELECT id, name, slug, owner_id, created_at FROM new_org\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "name", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "slug", + "type_info": "Text" + }, + { + "ordinal": 3, + "name": "owner_id", + "type_info": "Uuid" + }, + { + "ordinal": 4, + "name": "created_at", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [ + "Uuid", + "Text", + "Text", + "Uuid" + ] + }, + "nullable": [ + false, + false, + false, + false, + false + ] + }, + "hash": "a1cdd2d0e616783625de8cf307a359b8757df8911afaff0358e97b8b7769bf41" +} diff --git a/lynx/dashboard/server/.sqlx/query-a1ff2c0400bd15bd04e56cad71e6fd7948dec918bf1374fe9522f239e680d686.json b/lynx/dashboard/server/.sqlx/query-a1ff2c0400bd15bd04e56cad71e6fd7948dec918bf1374fe9522f239e680d686.json new file mode 100644 index 0000000..0cda45f --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-a1ff2c0400bd15bd04e56cad71e6fd7948dec918bf1374fe9522f239e680d686.json @@ -0,0 +1,14 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE security_alerts SET acknowledged_at = NOW() WHERE id = $1", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [] + }, + "hash": "a1ff2c0400bd15bd04e56cad71e6fd7948dec918bf1374fe9522f239e680d686" +} diff --git a/lynx/dashboard/server/.sqlx/query-a4356813f382dca9f97f38b504e7285fc77e79296e143e7c8b54c299e175b066.json b/lynx/dashboard/server/.sqlx/query-a4356813f382dca9f97f38b504e7285fc77e79296e143e7c8b54c299e175b066.json new file mode 100644 index 0000000..d78953d --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-a4356813f382dca9f97f38b504e7285fc77e79296e143e7c8b54c299e175b066.json @@ -0,0 +1,14 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE migration_state SET status='error', error_message=$1, updated_at=NOW() WHERE id=1", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [] + }, + "hash": "a4356813f382dca9f97f38b504e7285fc77e79296e143e7c8b54c299e175b066" +} diff --git a/lynx/dashboard/server/.sqlx/query-a70e3f0396f786f2559abd657a1f627c3b18973a8ac08b85290d8e6061a0f244.json b/lynx/dashboard/server/.sqlx/query-a70e3f0396f786f2559abd657a1f627c3b18973a8ac08b85290d8e6061a0f244.json new file mode 100644 index 0000000..05d0469 --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-a70e3f0396f786f2559abd657a1f627c3b18973a8ac08b85290d8e6061a0f244.json @@ -0,0 +1,14 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE domain_config SET status='error', error_message=$1, updated_at=NOW() WHERE id=1", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [] + }, + "hash": "a70e3f0396f786f2559abd657a1f627c3b18973a8ac08b85290d8e6061a0f244" +} diff --git a/lynx/dashboard/server/.sqlx/query-a8b72f2a5ffcc619153b52dd5fa49dd9e88545c22e267ceb680a94c090fbc739.json b/lynx/dashboard/server/.sqlx/query-a8b72f2a5ffcc619153b52dd5fa49dd9e88545c22e267ceb680a94c090fbc739.json new file mode 100644 index 0000000..1b220f7 --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-a8b72f2a5ffcc619153b52dd5fa49dd9e88545c22e267ceb680a94c090fbc739.json @@ -0,0 +1,20 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT ip::text AS ip FROM ip_pool WHERE agent_id IS NULL ORDER BY ip LIMIT 1 FOR UPDATE SKIP LOCKED", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "ip", + "type_info": "Text" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + null + ] + }, + "hash": "a8b72f2a5ffcc619153b52dd5fa49dd9e88545c22e267ceb680a94c090fbc739" +} diff --git a/lynx/dashboard/server/.sqlx/query-aa4a6f44dcfa7269ffc4c6bbf690a85cf44e28f547071dd8147da5191a8a41fb.json b/lynx/dashboard/server/.sqlx/query-aa4a6f44dcfa7269ffc4c6bbf690a85cf44e28f547071dd8147da5191a8a41fb.json new file mode 100644 index 0000000..5a432bc --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-aa4a6f44dcfa7269ffc4c6bbf690a85cf44e28f547071dd8147da5191a8a41fb.json @@ -0,0 +1,17 @@ +{ + "db_name": "PostgreSQL", + "query": "\n INSERT INTO update_log (id, triggered_by, version, channel, scope, agent_id, status)\n VALUES ($1, NULL, $2, 'stable', 'agent', $3, $4)\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid", + "Text", + "Uuid", + "Text" + ] + }, + "nullable": [] + }, + "hash": "aa4a6f44dcfa7269ffc4c6bbf690a85cf44e28f547071dd8147da5191a8a41fb" +} diff --git a/lynx/dashboard/server/.sqlx/query-ac06ef92f76ab45c3d54f5cf17ea36906062db80a2951acfff6ceac849643ad3.json b/lynx/dashboard/server/.sqlx/query-ac06ef92f76ab45c3d54f5cf17ea36906062db80a2951acfff6ceac849643ad3.json new file mode 100644 index 0000000..e4ba998 --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-ac06ef92f76ab45c3d54f5cf17ea36906062db80a2951acfff6ceac849643ad3.json @@ -0,0 +1,130 @@ +{ + "db_name": "PostgreSQL", + "query": "\n INSERT INTO nftables_rules\n (id, scope, agent_id, kind, port, port_end, protocol, ip_list, ip_version,\n rate_per_min, description, priority, direction, created_by)\n VALUES ($1, 'local', $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)\n RETURNING id, scope, agent_id, kind, port, port_end, protocol,\n ip_list, ip_version, rate_per_min, description,\n priority, enabled, direction, created_by, created_at, updated_at\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "scope", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "agent_id", + "type_info": "Uuid" + }, + { + "ordinal": 3, + "name": "kind", + "type_info": "Text" + }, + { + "ordinal": 4, + "name": "port", + "type_info": "Int4" + }, + { + "ordinal": 5, + "name": "port_end", + "type_info": "Int4" + }, + { + "ordinal": 6, + "name": "protocol", + "type_info": "Text" + }, + { + "ordinal": 7, + "name": "ip_list", + "type_info": "TextArray" + }, + { + "ordinal": 8, + "name": "ip_version", + "type_info": "Text" + }, + { + "ordinal": 9, + "name": "rate_per_min", + "type_info": "Int4" + }, + { + "ordinal": 10, + "name": "description", + "type_info": "Text" + }, + { + "ordinal": 11, + "name": "priority", + "type_info": "Int4" + }, + { + "ordinal": 12, + "name": "enabled", + "type_info": "Bool" + }, + { + "ordinal": 13, + "name": "direction", + "type_info": "Text" + }, + { + "ordinal": 14, + "name": "created_by", + "type_info": "Uuid" + }, + { + "ordinal": 15, + "name": "created_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 16, + "name": "updated_at", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [ + "Uuid", + "Uuid", + "Text", + "Int4", + "Int4", + "Text", + "TextArray", + "Text", + "Int4", + "Text", + "Int4", + "Text", + "Uuid" + ] + }, + "nullable": [ + false, + false, + true, + false, + true, + true, + true, + false, + false, + true, + true, + false, + false, + false, + true, + false, + false + ] + }, + "hash": "ac06ef92f76ab45c3d54f5cf17ea36906062db80a2951acfff6ceac849643ad3" +} diff --git a/lynx/dashboard/server/.sqlx/query-ad2c4af6faa02aff176a4fe887aca9937dc65d2faa7200997769d20944b408d8.json b/lynx/dashboard/server/.sqlx/query-ad2c4af6faa02aff176a4fe887aca9937dc65d2faa7200997769d20944b408d8.json new file mode 100644 index 0000000..de785a2 --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-ad2c4af6faa02aff176a4fe887aca9937dc65d2faa7200997769d20944b408d8.json @@ -0,0 +1,38 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT id, username, force_password_change, created_at FROM users ORDER BY created_at", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "username", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "force_password_change", + "type_info": "Bool" + }, + { + "ordinal": 3, + "name": "created_at", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + false, + false, + false, + false + ] + }, + "hash": "ad2c4af6faa02aff176a4fe887aca9937dc65d2faa7200997769d20944b408d8" +} diff --git a/lynx/dashboard/server/.sqlx/query-b2dfe766582aaba5f1dc08037546337c5aadcf8d596718669238bfea645e3ea0.json b/lynx/dashboard/server/.sqlx/query-b2dfe766582aaba5f1dc08037546337c5aadcf8d596718669238bfea645e3ea0.json new file mode 100644 index 0000000..32b98a7 --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-b2dfe766582aaba5f1dc08037546337c5aadcf8d596718669238bfea645e3ea0.json @@ -0,0 +1,68 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT id, domain, cert_type, cert_expires_at, hsts_enabled, port_19443_open, status, error_message, updated_at FROM domain_config WHERE id = 1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int4" + }, + { + "ordinal": 1, + "name": "domain", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "cert_type", + "type_info": "Text" + }, + { + "ordinal": 3, + "name": "cert_expires_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 4, + "name": "hsts_enabled", + "type_info": "Bool" + }, + { + "ordinal": 5, + "name": "port_19443_open", + "type_info": "Bool" + }, + { + "ordinal": 6, + "name": "status", + "type_info": "Text" + }, + { + "ordinal": 7, + "name": "error_message", + "type_info": "Text" + }, + { + "ordinal": 8, + "name": "updated_at", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + false, + true, + false, + true, + false, + false, + false, + true, + false + ] + }, + "hash": "b2dfe766582aaba5f1dc08037546337c5aadcf8d596718669238bfea645e3ea0" +} diff --git a/lynx/dashboard/server/.sqlx/query-b4680e78db63851fa00f8695dfbe7a868c543256e6401640de0d16165e330ad7.json b/lynx/dashboard/server/.sqlx/query-b4680e78db63851fa00f8695dfbe7a868c543256e6401640de0d16165e330ad7.json new file mode 100644 index 0000000..1bc6a59 --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-b4680e78db63851fa00f8695dfbe7a868c543256e6401640de0d16165e330ad7.json @@ -0,0 +1,20 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT id FROM agents WHERE status != 'online'", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + false + ] + }, + "hash": "b4680e78db63851fa00f8695dfbe7a868c543256e6401640de0d16165e330ad7" +} diff --git a/lynx/dashboard/server/.sqlx/query-b7136c6331f8ec94862950f35c109bca6fbae8e1f09099143cc75273b171ebef.json b/lynx/dashboard/server/.sqlx/query-b7136c6331f8ec94862950f35c109bca6fbae8e1f09099143cc75273b171ebef.json new file mode 100644 index 0000000..4b5c598 --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-b7136c6331f8ec94862950f35c109bca6fbae8e1f09099143cc75273b171ebef.json @@ -0,0 +1,15 @@ +{ + "db_name": "PostgreSQL", + "query": "\n INSERT INTO update_log (id, triggered_by, version, channel, scope, agent_id, status)\n VALUES ($1, NULL, $2, 'stable', 'dashboard', NULL, 'pending')\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid", + "Text" + ] + }, + "nullable": [] + }, + "hash": "b7136c6331f8ec94862950f35c109bca6fbae8e1f09099143cc75273b171ebef" +} diff --git a/lynx/dashboard/server/.sqlx/query-b721a02127fa5d53ac29fd661d789abcc317357595a6a838fa590ae2d9cabd53.json b/lynx/dashboard/server/.sqlx/query-b721a02127fa5d53ac29fd661d789abcc317357595a6a838fa590ae2d9cabd53.json new file mode 100644 index 0000000..39f3ced --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-b721a02127fa5d53ac29fd661d789abcc317357595a6a838fa590ae2d9cabd53.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT wg_pubkey FROM agents WHERE id = $1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "wg_pubkey", + "type_info": "Text" + } + ], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [ + false + ] + }, + "hash": "b721a02127fa5d53ac29fd661d789abcc317357595a6a838fa590ae2d9cabd53" +} diff --git a/lynx/dashboard/server/.sqlx/query-bcc7d725722d902786058ed6a2d436332ce30ec7961da355f33c3fb752d07059.json b/lynx/dashboard/server/.sqlx/query-bcc7d725722d902786058ed6a2d436332ce30ec7961da355f33c3fb752d07059.json new file mode 100644 index 0000000..ab3663a --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-bcc7d725722d902786058ed6a2d436332ce30ec7961da355f33c3fb752d07059.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT theme FROM user_preferences WHERE user_id = $1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "theme", + "type_info": "Text" + } + ], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [ + false + ] + }, + "hash": "bcc7d725722d902786058ed6a2d436332ce30ec7961da355f33c3fb752d07059" +} diff --git a/lynx/dashboard/server/.sqlx/query-bce32ae0cb0f46b91f0d47a5255a1d56d776ea83bff7c764b125301326068162.json b/lynx/dashboard/server/.sqlx/query-bce32ae0cb0f46b91f0d47a5255a1d56d776ea83bff7c764b125301326068162.json new file mode 100644 index 0000000..9e34659 --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-bce32ae0cb0f46b91f0d47a5255a1d56d776ea83bff7c764b125301326068162.json @@ -0,0 +1,14 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE agents SET status='offline' WHERE id=$1", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [] + }, + "hash": "bce32ae0cb0f46b91f0d47a5255a1d56d776ea83bff7c764b125301326068162" +} diff --git a/lynx/dashboard/server/.sqlx/query-bcf8f8546e55c422a00a2a0fe13ce803fc87f10716559b6dc7e6ea061aaecb8c.json b/lynx/dashboard/server/.sqlx/query-bcf8f8546e55c422a00a2a0fe13ce803fc87f10716559b6dc7e6ea061aaecb8c.json new file mode 100644 index 0000000..252663c --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-bcf8f8546e55c422a00a2a0fe13ce803fc87f10716559b6dc7e6ea061aaecb8c.json @@ -0,0 +1,26 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT domain, status FROM domain_config WHERE id = 1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "domain", + "type_info": "Text" + }, + { + "ordinal": 1, + "name": "status", + "type_info": "Text" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + true, + false + ] + }, + "hash": "bcf8f8546e55c422a00a2a0fe13ce803fc87f10716559b6dc7e6ea061aaecb8c" +} diff --git a/lynx/dashboard/server/.sqlx/query-be120dfdb84bf3d2a4aaa0f8304336a5cde84461ac2782323a9efa8a5fc7478d.json b/lynx/dashboard/server/.sqlx/query-be120dfdb84bf3d2a4aaa0f8304336a5cde84461ac2782323a9efa8a5fc7478d.json new file mode 100644 index 0000000..89f9496 --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-be120dfdb84bf3d2a4aaa0f8304336a5cde84461ac2782323a9efa8a5fc7478d.json @@ -0,0 +1,20 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT COUNT(DISTINCT ur.user_id) FROM user_roles ur\n JOIN role_permissions rp ON rp.role_id = ur.role_id\n JOIN permissions p ON p.id = rp.permission_id\n WHERE p.key = '*:*'", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "count", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + null + ] + }, + "hash": "be120dfdb84bf3d2a4aaa0f8304336a5cde84461ac2782323a9efa8a5fc7478d" +} diff --git a/lynx/dashboard/server/.sqlx/query-be3842946fe4e2eab3395bf6ac5385cbef60f14f19815da84ecbae6285450598.json b/lynx/dashboard/server/.sqlx/query-be3842946fe4e2eab3395bf6ac5385cbef60f14f19815da84ecbae6285450598.json new file mode 100644 index 0000000..49e2a0b --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-be3842946fe4e2eab3395bf6ac5385cbef60f14f19815da84ecbae6285450598.json @@ -0,0 +1,15 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO agent_events (id, agent_id, event, detail) VALUES ($1, $2, 'connected', NULL)", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid", + "Uuid" + ] + }, + "nullable": [] + }, + "hash": "be3842946fe4e2eab3395bf6ac5385cbef60f14f19815da84ecbae6285450598" +} diff --git a/lynx/dashboard/server/.sqlx/query-bf0829f55035d5c0c9ecc522dde8bf270cda957987d5609d3682eb9179a633e3.json b/lynx/dashboard/server/.sqlx/query-bf0829f55035d5c0c9ecc522dde8bf270cda957987d5609d3682eb9179a633e3.json new file mode 100644 index 0000000..89b7181 --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-bf0829f55035d5c0c9ecc522dde8bf270cda957987d5609d3682eb9179a633e3.json @@ -0,0 +1,16 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE agents SET cert_payload = $1, cert_signature = $2, cert_expires_at = NOW() + INTERVAL '90 days' WHERE id = $3", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Text", + "Text", + "Uuid" + ] + }, + "nullable": [] + }, + "hash": "bf0829f55035d5c0c9ecc522dde8bf270cda957987d5609d3682eb9179a633e3" +} diff --git a/lynx/dashboard/server/.sqlx/query-bf8a29d946b3ce005da228ea2b24ffa0e0577e8759bc47e58f042c55e27a4af3.json b/lynx/dashboard/server/.sqlx/query-bf8a29d946b3ce005da228ea2b24ffa0e0577e8759bc47e58f042c55e27a4af3.json new file mode 100644 index 0000000..5d48037 --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-bf8a29d946b3ce005da228ea2b24ffa0e0577e8759bc47e58f042c55e27a4af3.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT 1 FROM global_rule_sync WHERE agent_id = $1 AND synced_at IS NULL LIMIT 1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "?column?", + "type_info": "Int4" + } + ], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [ + null + ] + }, + "hash": "bf8a29d946b3ce005da228ea2b24ffa0e0577e8759bc47e58f042c55e27a4af3" +} diff --git a/lynx/dashboard/server/.sqlx/query-c0528d6051f201cbd154994d8baf135cb20ecc018c2204f642674c1da8cafacb.json b/lynx/dashboard/server/.sqlx/query-c0528d6051f201cbd154994d8baf135cb20ecc018c2204f642674c1da8cafacb.json new file mode 100644 index 0000000..593e727 --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-c0528d6051f201cbd154994d8baf135cb20ecc018c2204f642674c1da8cafacb.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT sync_token_hash FROM agents WHERE id=$1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "sync_token_hash", + "type_info": "Text" + } + ], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [ + true + ] + }, + "hash": "c0528d6051f201cbd154994d8baf135cb20ecc018c2204f642674c1da8cafacb" +} diff --git a/lynx/dashboard/server/.sqlx/query-c2ad4ef2a4f50273ad988e02b07b04c81e645e9a41485c7d9a8cb4e5198f2750.json b/lynx/dashboard/server/.sqlx/query-c2ad4ef2a4f50273ad988e02b07b04c81e645e9a41485c7d9a8cb4e5198f2750.json new file mode 100644 index 0000000..29169f1 --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-c2ad4ef2a4f50273ad988e02b07b04c81e645e9a41485c7d9a8cb4e5198f2750.json @@ -0,0 +1,15 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE users SET password_hash = $1, force_password_change = FALSE WHERE id = $2", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Text", + "Uuid" + ] + }, + "nullable": [] + }, + "hash": "c2ad4ef2a4f50273ad988e02b07b04c81e645e9a41485c7d9a8cb4e5198f2750" +} diff --git a/lynx/dashboard/server/.sqlx/query-c3445902b5d334063f62d3e2c219f9924645e1bfb78df5e52bb66b1d640cd744.json b/lynx/dashboard/server/.sqlx/query-c3445902b5d334063f62d3e2c219f9924645e1bfb78df5e52bb66b1d640cd744.json new file mode 100644 index 0000000..8d4b2a0 --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-c3445902b5d334063f62d3e2c219f9924645e1bfb78df5e52bb66b1d640cd744.json @@ -0,0 +1,14 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE migration_state SET status='waiting_agents', agents_total=$1, updated_at=NOW() WHERE id=1", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int4" + ] + }, + "nullable": [] + }, + "hash": "c3445902b5d334063f62d3e2c219f9924645e1bfb78df5e52bb66b1d640cd744" +} diff --git a/lynx/dashboard/server/.sqlx/query-c359906fc42e182370256bf302c022374bfc665864fd14da9852556537dd62e3.json b/lynx/dashboard/server/.sqlx/query-c359906fc42e182370256bf302c022374bfc665864fd14da9852556537dd62e3.json new file mode 100644 index 0000000..4c41b11 --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-c359906fc42e182370256bf302c022374bfc665864fd14da9852556537dd62e3.json @@ -0,0 +1,44 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT id, kind, detail, agent_id, created_at\n FROM security_alerts\n WHERE acknowledged_at IS NULL\n ORDER BY created_at DESC\n LIMIT 100", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "kind", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "detail", + "type_info": "Text" + }, + { + "ordinal": 3, + "name": "agent_id", + "type_info": "Uuid" + }, + { + "ordinal": 4, + "name": "created_at", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + false, + false, + true, + true, + false + ] + }, + "hash": "c359906fc42e182370256bf302c022374bfc665864fd14da9852556537dd62e3" +} diff --git a/lynx/dashboard/server/.sqlx/query-c4f83d35f2074769fea148a0459a24bbe401d91c23501e334b2b6a317886cc9a.json b/lynx/dashboard/server/.sqlx/query-c4f83d35f2074769fea148a0459a24bbe401d91c23501e334b2b6a317886cc9a.json new file mode 100644 index 0000000..fb48ce4 --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-c4f83d35f2074769fea148a0459a24bbe401d91c23501e334b2b6a317886cc9a.json @@ -0,0 +1,28 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT p.id, p.key FROM role_permissions rp JOIN permissions p ON p.id = rp.permission_id WHERE rp.role_id = $1 ORDER BY p.key", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "key", + "type_info": "Text" + } + ], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [ + false, + false + ] + }, + "hash": "c4f83d35f2074769fea148a0459a24bbe401d91c23501e334b2b6a317886cc9a" +} diff --git a/lynx/dashboard/server/.sqlx/query-c69f4c85dfbc06b1009e4dea65142078f4a7cdb5a0c6145f2a417a0a5e0295b9.json b/lynx/dashboard/server/.sqlx/query-c69f4c85dfbc06b1009e4dea65142078f4a7cdb5a0c6145f2a417a0a5e0295b9.json new file mode 100644 index 0000000..47dc4b7 --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-c69f4c85dfbc06b1009e4dea65142078f4a7cdb5a0c6145f2a417a0a5e0295b9.json @@ -0,0 +1,20 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT COUNT(*) FROM data_plane_tunnels", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "count", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + null + ] + }, + "hash": "c69f4c85dfbc06b1009e4dea65142078f4a7cdb5a0c6145f2a417a0a5e0295b9" +} diff --git a/lynx/dashboard/server/.sqlx/query-caefed268a457a3df763fe27c395ede9b0179463756eb78c9ad733cd52280e26.json b/lynx/dashboard/server/.sqlx/query-caefed268a457a3df763fe27c395ede9b0179463756eb78c9ad733cd52280e26.json new file mode 100644 index 0000000..1f2b4ef --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-caefed268a457a3df763fe27c395ede9b0179463756eb78c9ad733cd52280e26.json @@ -0,0 +1,20 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT value FROM system_config WHERE key = 'setup_token_issued_at'", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "value", + "type_info": "Text" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + false + ] + }, + "hash": "caefed268a457a3df763fe27c395ede9b0179463756eb78c9ad733cd52280e26" +} diff --git a/lynx/dashboard/server/.sqlx/query-cde1a0d0f7444e516b041918065ee9249a3dfce2f5c7d9d48cc8365f6c5d36fe.json b/lynx/dashboard/server/.sqlx/query-cde1a0d0f7444e516b041918065ee9249a3dfce2f5c7d9d48cc8365f6c5d36fe.json new file mode 100644 index 0000000..d6bd8f0 --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-cde1a0d0f7444e516b041918065ee9249a3dfce2f5c7d9d48cc8365f6c5d36fe.json @@ -0,0 +1,38 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT id, wg_ip, api_port, status FROM agents WHERE status = 'online'", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "wg_ip", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "api_port", + "type_info": "Int4" + }, + { + "ordinal": 3, + "name": "status", + "type_info": "Text" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + false, + false, + false, + false + ] + }, + "hash": "cde1a0d0f7444e516b041918065ee9249a3dfce2f5c7d9d48cc8365f6c5d36fe" +} diff --git a/lynx/dashboard/server/.sqlx/query-ce5d92880df07764b51b9ce1869930b72ba1d4d642a607a719c3ac45bb252595.json b/lynx/dashboard/server/.sqlx/query-ce5d92880df07764b51b9ce1869930b72ba1d4d642a607a719c3ac45bb252595.json new file mode 100644 index 0000000..d41de56 --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-ce5d92880df07764b51b9ce1869930b72ba1d4d642a607a719c3ac45bb252595.json @@ -0,0 +1,38 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT id, wg_ip, api_port, status FROM agents", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "wg_ip", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "api_port", + "type_info": "Int4" + }, + { + "ordinal": 3, + "name": "status", + "type_info": "Text" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + false, + false, + false, + false + ] + }, + "hash": "ce5d92880df07764b51b9ce1869930b72ba1d4d642a607a719c3ac45bb252595" +} diff --git a/lynx/dashboard/server/.sqlx/query-cf1514bfec20e40e19443a39e371d2792cafb0b80fea5a8ca7df57ea3c8f6e7f.json b/lynx/dashboard/server/.sqlx/query-cf1514bfec20e40e19443a39e371d2792cafb0b80fea5a8ca7df57ea3c8f6e7f.json new file mode 100644 index 0000000..b44e670 --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-cf1514bfec20e40e19443a39e371d2792cafb0b80fea5a8ca7df57ea3c8f6e7f.json @@ -0,0 +1,40 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT id, wg_ip::text AS wg_ip, api_port, COALESCE(arch, 'x86_64') AS arch FROM agents WHERE status = 'online' AND (version IS NULL OR version != $1)", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "wg_ip", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "api_port", + "type_info": "Int4" + }, + { + "ordinal": 3, + "name": "arch", + "type_info": "Text" + } + ], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [ + false, + false, + false, + null + ] + }, + "hash": "cf1514bfec20e40e19443a39e371d2792cafb0b80fea5a8ca7df57ea3c8f6e7f" +} diff --git a/lynx/dashboard/server/.sqlx/query-cf86def10b30f5d8a43d5847fb8699b53f5f8f0a78f5765028c93d29434a2143.json b/lynx/dashboard/server/.sqlx/query-cf86def10b30f5d8a43d5847fb8699b53f5f8f0a78f5765028c93d29434a2143.json new file mode 100644 index 0000000..86e5be7 --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-cf86def10b30f5d8a43d5847fb8699b53f5f8f0a78f5765028c93d29434a2143.json @@ -0,0 +1,20 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT wg_pubkey FROM agents", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "wg_pubkey", + "type_info": "Text" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + false + ] + }, + "hash": "cf86def10b30f5d8a43d5847fb8699b53f5f8f0a78f5765028c93d29434a2143" +} diff --git a/lynx/dashboard/server/.sqlx/query-d1153950633136e4f1c754e26e50f89f192c3d122ace0f02e46856a6d92dd7fb.json b/lynx/dashboard/server/.sqlx/query-d1153950633136e4f1c754e26e50f89f192c3d122ace0f02e46856a6d92dd7fb.json new file mode 100644 index 0000000..2a227da --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-d1153950633136e4f1c754e26e50f89f192c3d122ace0f02e46856a6d92dd7fb.json @@ -0,0 +1,34 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT id, wg_ip, api_port\n FROM agents\n WHERE status = 'online'\n AND (cert_expires_at IS NULL OR cert_expires_at < NOW() + ($1 || ' days')::INTERVAL)\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "wg_ip", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "api_port", + "type_info": "Int4" + } + ], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [ + false, + false, + false + ] + }, + "hash": "d1153950633136e4f1c754e26e50f89f192c3d122ace0f02e46856a6d92dd7fb" +} diff --git a/lynx/dashboard/server/.sqlx/query-d3d8affd6da972e5b1b0502d4687745a98097b59965f5960584f0c2d3c74d3e8.json b/lynx/dashboard/server/.sqlx/query-d3d8affd6da972e5b1b0502d4687745a98097b59965f5960584f0c2d3c74d3e8.json new file mode 100644 index 0000000..acf7765 --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-d3d8affd6da972e5b1b0502d4687745a98097b59965f5960584f0c2d3c74d3e8.json @@ -0,0 +1,23 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT 1 FROM organization_members WHERE organization_id = $1 AND user_id = $2", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "?column?", + "type_info": "Int4" + } + ], + "parameters": { + "Left": [ + "Uuid", + "Uuid" + ] + }, + "nullable": [ + null + ] + }, + "hash": "d3d8affd6da972e5b1b0502d4687745a98097b59965f5960584f0c2d3c74d3e8" +} diff --git a/lynx/dashboard/server/.sqlx/query-d6fe7969e280a11aafa7c6686ce5f5c37498545f8a11deb3b86f0af2295d91cc.json b/lynx/dashboard/server/.sqlx/query-d6fe7969e280a11aafa7c6686ce5f5c37498545f8a11deb3b86f0af2295d91cc.json new file mode 100644 index 0000000..990c73a --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-d6fe7969e280a11aafa7c6686ce5f5c37498545f8a11deb3b86f0af2295d91cc.json @@ -0,0 +1,23 @@ +{ + "db_name": "PostgreSQL", + "query": "DELETE FROM nftables_rules WHERE id = $1 AND scope = 'local' AND agent_id = $2 RETURNING id", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + } + ], + "parameters": { + "Left": [ + "Uuid", + "Uuid" + ] + }, + "nullable": [ + false + ] + }, + "hash": "d6fe7969e280a11aafa7c6686ce5f5c37498545f8a11deb3b86f0af2295d91cc" +} diff --git a/lynx/dashboard/server/.sqlx/query-d99827a5ef0b4c3de6ac18e84f65a8667184370fb27a2476ce8d2490d072454c.json b/lynx/dashboard/server/.sqlx/query-d99827a5ef0b4c3de6ac18e84f65a8667184370fb27a2476ce8d2490d072454c.json new file mode 100644 index 0000000..8433d4a --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-d99827a5ef0b4c3de6ac18e84f65a8667184370fb27a2476ce8d2490d072454c.json @@ -0,0 +1,19 @@ +{ + "db_name": "PostgreSQL", + "query": "\n INSERT INTO users (id, username, email_hash, email_encrypted, password_hash, dek_encrypted)\n VALUES ($1, $2, $3, $4, $5, $6)\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid", + "Text", + "Text", + "Bytea", + "Text", + "Bytea" + ] + }, + "nullable": [] + }, + "hash": "d99827a5ef0b4c3de6ac18e84f65a8667184370fb27a2476ce8d2490d072454c" +} diff --git a/lynx/dashboard/server/.sqlx/query-dd99e48b1572e25db38f03da95984fda1072913b29bb6b3753a0d351583dfff6.json b/lynx/dashboard/server/.sqlx/query-dd99e48b1572e25db38f03da95984fda1072913b29bb6b3753a0d351583dfff6.json new file mode 100644 index 0000000..74b2594 --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-dd99e48b1572e25db38f03da95984fda1072913b29bb6b3753a0d351583dfff6.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT id FROM users WHERE username = $1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + } + ], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [ + false + ] + }, + "hash": "dd99e48b1572e25db38f03da95984fda1072913b29bb6b3753a0d351583dfff6" +} diff --git a/lynx/dashboard/server/.sqlx/query-de84ba69402ef3bd9cddb7cf14f4c422305475d42c26eecc4049b08a220ce402.json b/lynx/dashboard/server/.sqlx/query-de84ba69402ef3bd9cddb7cf14f4c422305475d42c26eecc4049b08a220ce402.json new file mode 100644 index 0000000..5dd60e4 --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-de84ba69402ef3bd9cddb7cf14f4c422305475d42c26eecc4049b08a220ce402.json @@ -0,0 +1,14 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE data_plane_tunnels SET status='torn_down', updated_at=NOW() WHERE id=$1", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [] + }, + "hash": "de84ba69402ef3bd9cddb7cf14f4c422305475d42c26eecc4049b08a220ce402" +} diff --git a/lynx/dashboard/server/.sqlx/query-dfd03e46dec4bd17decd27af0f5e06cbc8b69fc8b29889638f94bad1b7418365.json b/lynx/dashboard/server/.sqlx/query-dfd03e46dec4bd17decd27af0f5e06cbc8b69fc8b29889638f94bad1b7418365.json new file mode 100644 index 0000000..a7f27f4 --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-dfd03e46dec4bd17decd27af0f5e06cbc8b69fc8b29889638f94bad1b7418365.json @@ -0,0 +1,116 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT id, scope, agent_id, kind, port, port_end, protocol,\n ip_list, ip_version, rate_per_min, description,\n priority, enabled, direction, created_by, created_at, updated_at\n FROM nftables_rules\n WHERE scope = 'global' AND enabled = true\n ORDER BY priority ASC, created_at ASC\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "scope", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "agent_id", + "type_info": "Uuid" + }, + { + "ordinal": 3, + "name": "kind", + "type_info": "Text" + }, + { + "ordinal": 4, + "name": "port", + "type_info": "Int4" + }, + { + "ordinal": 5, + "name": "port_end", + "type_info": "Int4" + }, + { + "ordinal": 6, + "name": "protocol", + "type_info": "Text" + }, + { + "ordinal": 7, + "name": "ip_list", + "type_info": "TextArray" + }, + { + "ordinal": 8, + "name": "ip_version", + "type_info": "Text" + }, + { + "ordinal": 9, + "name": "rate_per_min", + "type_info": "Int4" + }, + { + "ordinal": 10, + "name": "description", + "type_info": "Text" + }, + { + "ordinal": 11, + "name": "priority", + "type_info": "Int4" + }, + { + "ordinal": 12, + "name": "enabled", + "type_info": "Bool" + }, + { + "ordinal": 13, + "name": "direction", + "type_info": "Text" + }, + { + "ordinal": 14, + "name": "created_by", + "type_info": "Uuid" + }, + { + "ordinal": 15, + "name": "created_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 16, + "name": "updated_at", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + false, + false, + true, + false, + true, + true, + true, + false, + false, + true, + true, + false, + false, + false, + true, + false, + false + ] + }, + "hash": "dfd03e46dec4bd17decd27af0f5e06cbc8b69fc8b29889638f94bad1b7418365" +} diff --git a/lynx/dashboard/server/.sqlx/query-dffde846539a7ccf1a63cc2378f4f5d1ed7d60c34d074642e34a6970d3972a13.json b/lynx/dashboard/server/.sqlx/query-dffde846539a7ccf1a63cc2378f4f5d1ed7d60c34d074642e34a6970d3972a13.json new file mode 100644 index 0000000..77cc6b1 --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-dffde846539a7ccf1a63cc2378f4f5d1ed7d60c34d074642e34a6970d3972a13.json @@ -0,0 +1,46 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT id, agent_id, event, detail, created_at\n FROM agent_events\n ORDER BY created_at DESC\n LIMIT $1\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "agent_id", + "type_info": "Uuid" + }, + { + "ordinal": 2, + "name": "event", + "type_info": "Text" + }, + { + "ordinal": 3, + "name": "detail", + "type_info": "Text" + }, + { + "ordinal": 4, + "name": "created_at", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [ + false, + false, + false, + true, + false + ] + }, + "hash": "dffde846539a7ccf1a63cc2378f4f5d1ed7d60c34d074642e34a6970d3972a13" +} diff --git a/lynx/dashboard/server/.sqlx/query-e0343546b4d839bf052229dcdafb90d801a3ca916849a363869bba605b648b07.json b/lynx/dashboard/server/.sqlx/query-e0343546b4d839bf052229dcdafb90d801a3ca916849a363869bba605b648b07.json new file mode 100644 index 0000000..e663772 --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-e0343546b4d839bf052229dcdafb90d801a3ca916849a363869bba605b648b07.json @@ -0,0 +1,15 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE migration_state\n SET status='transferring', role='source', target_url=$1,\n agents_total=$2, agents_confirmed=0,\n started_at=NOW(), updated_at=NOW()\n WHERE id=1", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Text", + "Int4" + ] + }, + "nullable": [] + }, + "hash": "e0343546b4d839bf052229dcdafb90d801a3ca916849a363869bba605b648b07" +} diff --git a/lynx/dashboard/server/.sqlx/query-e0458a70ef71f355e2afae88227d0e53b4c11b14dadb199323c766c6de8e9dd2.json b/lynx/dashboard/server/.sqlx/query-e0458a70ef71f355e2afae88227d0e53b4c11b14dadb199323c766c6de8e9dd2.json new file mode 100644 index 0000000..bc96739 --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-e0458a70ef71f355e2afae88227d0e53b4c11b14dadb199323c766c6de8e9dd2.json @@ -0,0 +1,23 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT role FROM organization_members WHERE organization_id = $1 AND user_id = $2", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "role", + "type_info": "Text" + } + ], + "parameters": { + "Left": [ + "Uuid", + "Uuid" + ] + }, + "nullable": [ + false + ] + }, + "hash": "e0458a70ef71f355e2afae88227d0e53b4c11b14dadb199323c766c6de8e9dd2" +} diff --git a/lynx/dashboard/server/.sqlx/query-e0c30c23263f4228d4fdd5f06c949d0dd2e6d6584eb51c00e191e70404e4915e.json b/lynx/dashboard/server/.sqlx/query-e0c30c23263f4228d4fdd5f06c949d0dd2e6d6584eb51c00e191e70404e4915e.json new file mode 100644 index 0000000..13fb194 --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-e0c30c23263f4228d4fdd5f06c949d0dd2e6d6584eb51c00e191e70404e4915e.json @@ -0,0 +1,76 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT id, name, wg_pubkey, wg_ip, wg_endpoint, api_port, status, version, last_heartbeat, created_at FROM agents WHERE id = $1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "name", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "wg_pubkey", + "type_info": "Text" + }, + { + "ordinal": 3, + "name": "wg_ip", + "type_info": "Text" + }, + { + "ordinal": 4, + "name": "wg_endpoint", + "type_info": "Text" + }, + { + "ordinal": 5, + "name": "api_port", + "type_info": "Int4" + }, + { + "ordinal": 6, + "name": "status", + "type_info": "Text" + }, + { + "ordinal": 7, + "name": "version", + "type_info": "Text" + }, + { + "ordinal": 8, + "name": "last_heartbeat", + "type_info": "Timestamptz" + }, + { + "ordinal": 9, + "name": "created_at", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [ + false, + false, + false, + false, + true, + false, + false, + true, + true, + false + ] + }, + "hash": "e0c30c23263f4228d4fdd5f06c949d0dd2e6d6584eb51c00e191e70404e4915e" +} diff --git a/lynx/dashboard/server/.sqlx/query-e0e32c83a767ed536f34a71493e3d4b195a7b80662e7cfe54c56d63ba2f731a9.json b/lynx/dashboard/server/.sqlx/query-e0e32c83a767ed536f34a71493e3d4b195a7b80662e7cfe54c56d63ba2f731a9.json new file mode 100644 index 0000000..11af083 --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-e0e32c83a767ed536f34a71493e3d4b195a7b80662e7cfe54c56d63ba2f731a9.json @@ -0,0 +1,17 @@ +{ + "db_name": "PostgreSQL", + "query": "\n UPDATE sessions\n SET refresh_token_hash = $1, last_used_at = NOW(), last_jti = $4\n WHERE id = $2 AND refresh_token_hash = $3\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Text", + "Uuid", + "Text", + "Uuid" + ] + }, + "nullable": [] + }, + "hash": "e0e32c83a767ed536f34a71493e3d4b195a7b80662e7cfe54c56d63ba2f731a9" +} diff --git a/lynx/dashboard/server/.sqlx/query-e23e31841c153d6358592d735535b4adc86eef2d73058da925e127886b4049bd.json b/lynx/dashboard/server/.sqlx/query-e23e31841c153d6358592d735535b4adc86eef2d73058da925e127886b4049bd.json new file mode 100644 index 0000000..b40bbad --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-e23e31841c153d6358592d735535b4adc86eef2d73058da925e127886b4049bd.json @@ -0,0 +1,20 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT status FROM migration_state WHERE id=1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "status", + "type_info": "Text" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + false + ] + }, + "hash": "e23e31841c153d6358592d735535b4adc86eef2d73058da925e127886b4049bd" +} diff --git a/lynx/dashboard/server/.sqlx/query-e37a101eba72ca88463656ac48cfcc0528b1b78e3f6bf8839ddb42ed05af1fe7.json b/lynx/dashboard/server/.sqlx/query-e37a101eba72ca88463656ac48cfcc0528b1b78e3f6bf8839ddb42ed05af1fe7.json new file mode 100644 index 0000000..ba65441 --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-e37a101eba72ca88463656ac48cfcc0528b1b78e3f6bf8839ddb42ed05af1fe7.json @@ -0,0 +1,28 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT username, single_session FROM users WHERE id = $1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "username", + "type_info": "Text" + }, + { + "ordinal": 1, + "name": "single_session", + "type_info": "Bool" + } + ], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [ + false, + false + ] + }, + "hash": "e37a101eba72ca88463656ac48cfcc0528b1b78e3f6bf8839ddb42ed05af1fe7" +} diff --git a/lynx/dashboard/server/.sqlx/query-e42b60278b57d4d0c49bd78fd17862019b6c025124453611dabed9c8b589484d.json b/lynx/dashboard/server/.sqlx/query-e42b60278b57d4d0c49bd78fd17862019b6c025124453611dabed9c8b589484d.json new file mode 100644 index 0000000..fdbaef5 --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-e42b60278b57d4d0c49bd78fd17862019b6c025124453611dabed9c8b589484d.json @@ -0,0 +1,76 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT id, project_id, agent_a_id, agent_b_id, agent_a_wg_ip, agent_b_wg_ip,\n wg_port, replica_count, status, created_at\n FROM data_plane_tunnels\n WHERE project_id = $1 AND status != 'torn_down'\n ORDER BY created_at ASC", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "project_id", + "type_info": "Uuid" + }, + { + "ordinal": 2, + "name": "agent_a_id", + "type_info": "Uuid" + }, + { + "ordinal": 3, + "name": "agent_b_id", + "type_info": "Uuid" + }, + { + "ordinal": 4, + "name": "agent_a_wg_ip", + "type_info": "Text" + }, + { + "ordinal": 5, + "name": "agent_b_wg_ip", + "type_info": "Text" + }, + { + "ordinal": 6, + "name": "wg_port", + "type_info": "Int4" + }, + { + "ordinal": 7, + "name": "replica_count", + "type_info": "Int4" + }, + { + "ordinal": 8, + "name": "status", + "type_info": "Text" + }, + { + "ordinal": 9, + "name": "created_at", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [ + false, + false, + false, + false, + false, + false, + false, + false, + false, + false + ] + }, + "hash": "e42b60278b57d4d0c49bd78fd17862019b6c025124453611dabed9c8b589484d" +} diff --git a/lynx/dashboard/server/.sqlx/query-e9ee477fc969775d4a868a773162a3d14a8bdb38cbdad2069ecea6b100bee629.json b/lynx/dashboard/server/.sqlx/query-e9ee477fc969775d4a868a773162a3d14a8bdb38cbdad2069ecea6b100bee629.json new file mode 100644 index 0000000..d2e871a --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-e9ee477fc969775d4a868a773162a3d14a8bdb38cbdad2069ecea6b100bee629.json @@ -0,0 +1,14 @@ +{ + "db_name": "PostgreSQL", + "query": "DELETE FROM sessions WHERE user_id = $1", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [] + }, + "hash": "e9ee477fc969775d4a868a773162a3d14a8bdb38cbdad2069ecea6b100bee629" +} diff --git a/lynx/dashboard/server/.sqlx/query-ec380f0eb30d86139ae6c5b773361f9052ac74ed48761317f73e94d5e483797e.json b/lynx/dashboard/server/.sqlx/query-ec380f0eb30d86139ae6c5b773361f9052ac74ed48761317f73e94d5e483797e.json new file mode 100644 index 0000000..41945ec --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-ec380f0eb30d86139ae6c5b773361f9052ac74ed48761317f73e94d5e483797e.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT key FROM permissions WHERE id = $1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "key", + "type_info": "Text" + } + ], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [ + false + ] + }, + "hash": "ec380f0eb30d86139ae6c5b773361f9052ac74ed48761317f73e94d5e483797e" +} diff --git a/lynx/dashboard/server/.sqlx/query-ec9246d45e3451b65f67d8414ab7a63036970f7cda57b5c17e0b57447963586c.json b/lynx/dashboard/server/.sqlx/query-ec9246d45e3451b65f67d8414ab7a63036970f7cda57b5c17e0b57447963586c.json new file mode 100644 index 0000000..fc2585f --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-ec9246d45e3451b65f67d8414ab7a63036970f7cda57b5c17e0b57447963586c.json @@ -0,0 +1,47 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT o.id, o.name, o.slug, o.owner_id, o.created_at\n FROM organizations o\n JOIN organization_members m ON m.organization_id = o.id\n WHERE o.id = $1 AND m.user_id = $2\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "name", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "slug", + "type_info": "Text" + }, + { + "ordinal": 3, + "name": "owner_id", + "type_info": "Uuid" + }, + { + "ordinal": 4, + "name": "created_at", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [ + "Uuid", + "Uuid" + ] + }, + "nullable": [ + false, + false, + false, + false, + false + ] + }, + "hash": "ec9246d45e3451b65f67d8414ab7a63036970f7cda57b5c17e0b57447963586c" +} diff --git a/lynx/dashboard/server/.sqlx/query-ef6ac9fe6505a4a987ae6406630167b3caa2b04fc004e62f57d6a5fcdc8bdabe.json b/lynx/dashboard/server/.sqlx/query-ef6ac9fe6505a4a987ae6406630167b3caa2b04fc004e62f57d6a5fcdc8bdabe.json new file mode 100644 index 0000000..4212d2b --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-ef6ac9fe6505a4a987ae6406630167b3caa2b04fc004e62f57d6a5fcdc8bdabe.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT COUNT(DISTINCT ur.user_id) FROM user_roles ur WHERE ur.role_id = $1\n AND NOT EXISTS (\n SELECT 1 FROM user_roles ur2\n JOIN role_permissions rp2 ON rp2.role_id = ur2.role_id\n JOIN permissions p2 ON p2.id = rp2.permission_id\n WHERE ur2.user_id = ur.user_id AND ur2.role_id != $1 AND p2.key = '*:*'\n )", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "count", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [ + null + ] + }, + "hash": "ef6ac9fe6505a4a987ae6406630167b3caa2b04fc004e62f57d6a5fcdc8bdabe" +} diff --git a/lynx/dashboard/server/.sqlx/query-f13916ea4ced9c5abe7f12e162508b64f3cd51152586dcff67cdd4c1bc18399a.json b/lynx/dashboard/server/.sqlx/query-f13916ea4ced9c5abe7f12e162508b64f3cd51152586dcff67cdd4c1bc18399a.json new file mode 100644 index 0000000..c582670 --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-f13916ea4ced9c5abe7f12e162508b64f3cd51152586dcff67cdd4c1bc18399a.json @@ -0,0 +1,34 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT id, detail, created_at\n FROM agent_events\n WHERE agent_id = $1\n AND event = 'nftables_divergence'\n AND created_at > COALESCE(\n (SELECT created_at FROM agent_events\n WHERE agent_id = $1 AND event IN ('nftables_restored', 'nftables_accepted')\n ORDER BY created_at DESC LIMIT 1),\n '1970-01-01'::timestamptz\n )\n ORDER BY created_at DESC\n LIMIT 1\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "detail", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "created_at", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [ + false, + true, + false + ] + }, + "hash": "f13916ea4ced9c5abe7f12e162508b64f3cd51152586dcff67cdd4c1bc18399a" +} diff --git a/lynx/dashboard/server/.sqlx/query-f4c6a65036b9705f43dd8a77bb46b2aad4cd543286d76ebd4580b98f6cb92f66.json b/lynx/dashboard/server/.sqlx/query-f4c6a65036b9705f43dd8a77bb46b2aad4cd543286d76ebd4580b98f6cb92f66.json new file mode 100644 index 0000000..0756265 --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-f4c6a65036b9705f43dd8a77bb46b2aad4cd543286d76ebd4580b98f6cb92f66.json @@ -0,0 +1,14 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE domain_config SET hsts_enabled=$1, updated_at=NOW() WHERE id=1", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Bool" + ] + }, + "nullable": [] + }, + "hash": "f4c6a65036b9705f43dd8a77bb46b2aad4cd543286d76ebd4580b98f6cb92f66" +} diff --git a/lynx/dashboard/server/.sqlx/query-f4cf1b2b88d9cb88a0d2ef8bc6b2e4e48570a40563f7a7e29b863048f426ad15.json b/lynx/dashboard/server/.sqlx/query-f4cf1b2b88d9cb88a0d2ef8bc6b2e4e48570a40563f7a7e29b863048f426ad15.json new file mode 100644 index 0000000..3f528d7 --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-f4cf1b2b88d9cb88a0d2ef8bc6b2e4e48570a40563f7a7e29b863048f426ad15.json @@ -0,0 +1,16 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO agent_events (id, agent_id, event, detail) VALUES ($1, $2, 'audit_integrity_failure', $3)", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid", + "Uuid", + "Text" + ] + }, + "nullable": [] + }, + "hash": "f4cf1b2b88d9cb88a0d2ef8bc6b2e4e48570a40563f7a7e29b863048f426ad15" +} diff --git a/lynx/dashboard/server/.sqlx/query-f7a980eee80b1db3ebe15d7e2a13540d4f12a1ad6c7d05f04f30a66950eb4e34.json b/lynx/dashboard/server/.sqlx/query-f7a980eee80b1db3ebe15d7e2a13540d4f12a1ad6c7d05f04f30a66950eb4e34.json new file mode 100644 index 0000000..4c10d0d --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-f7a980eee80b1db3ebe15d7e2a13540d4f12a1ad6c7d05f04f30a66950eb4e34.json @@ -0,0 +1,32 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT id, wg_ip, api_port FROM agents WHERE status = 'online'", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "wg_ip", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "api_port", + "type_info": "Int4" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + false, + false, + false + ] + }, + "hash": "f7a980eee80b1db3ebe15d7e2a13540d4f12a1ad6c7d05f04f30a66950eb4e34" +} diff --git a/lynx/dashboard/server/.sqlx/query-f89ee4d691ccc597f13408f072c47404ca4f5550232cbe81b0437eea3d7441da.json b/lynx/dashboard/server/.sqlx/query-f89ee4d691ccc597f13408f072c47404ca4f5550232cbe81b0437eea3d7441da.json new file mode 100644 index 0000000..948c6f1 --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-f89ee4d691ccc597f13408f072c47404ca4f5550232cbe81b0437eea3d7441da.json @@ -0,0 +1,14 @@ +{ + "db_name": "PostgreSQL", + "query": "DELETE FROM agents WHERE id = $1", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [] + }, + "hash": "f89ee4d691ccc597f13408f072c47404ca4f5550232cbe81b0437eea3d7441da" +} diff --git a/lynx/dashboard/server/.sqlx/query-fb1228f2d4d9b803809844c0dabaf042551275ffeeccd60485a707f1ddbe9c54.json b/lynx/dashboard/server/.sqlx/query-fb1228f2d4d9b803809844c0dabaf042551275ffeeccd60485a707f1ddbe9c54.json new file mode 100644 index 0000000..add53b0 --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-fb1228f2d4d9b803809844c0dabaf042551275ffeeccd60485a707f1ddbe9c54.json @@ -0,0 +1,72 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT id, agent_id, organization_id, user_id,\n command_type, result, error, entry_hash, created_at\n FROM audit_log\n WHERE agent_id = $1\n ORDER BY created_at DESC\n LIMIT $2 OFFSET $3\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "agent_id", + "type_info": "Uuid" + }, + { + "ordinal": 2, + "name": "organization_id", + "type_info": "Uuid" + }, + { + "ordinal": 3, + "name": "user_id", + "type_info": "Uuid" + }, + { + "ordinal": 4, + "name": "command_type", + "type_info": "Text" + }, + { + "ordinal": 5, + "name": "result", + "type_info": "Text" + }, + { + "ordinal": 6, + "name": "error", + "type_info": "Text" + }, + { + "ordinal": 7, + "name": "entry_hash", + "type_info": "Text" + }, + { + "ordinal": 8, + "name": "created_at", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [ + "Uuid", + "Int8", + "Int8" + ] + }, + "nullable": [ + false, + false, + true, + true, + false, + false, + true, + false, + false + ] + }, + "hash": "fb1228f2d4d9b803809844c0dabaf042551275ffeeccd60485a707f1ddbe9c54" +} diff --git a/lynx/dashboard/server/.sqlx/query-fb26486d49ed0f35aa74a29a272fda513666210cf3d047d353d33e2a3d7b8a02.json b/lynx/dashboard/server/.sqlx/query-fb26486d49ed0f35aa74a29a272fda513666210cf3d047d353d33e2a3d7b8a02.json new file mode 100644 index 0000000..70c23aa --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-fb26486d49ed0f35aa74a29a272fda513666210cf3d047d353d33e2a3d7b8a02.json @@ -0,0 +1,28 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT id, password_hash FROM users WHERE id = $1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "password_hash", + "type_info": "Text" + } + ], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [ + false, + false + ] + }, + "hash": "fb26486d49ed0f35aa74a29a272fda513666210cf3d047d353d33e2a3d7b8a02" +} diff --git a/lynx/dashboard/server/.sqlx/query-fb64d4fb0ba9c699f1842b4ba59677e401c7f02c8ee45c1d633752755ec186f4.json b/lynx/dashboard/server/.sqlx/query-fb64d4fb0ba9c699f1842b4ba59677e401c7f02c8ee45c1d633752755ec186f4.json new file mode 100644 index 0000000..b619c62 --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-fb64d4fb0ba9c699f1842b4ba59677e401c7f02c8ee45c1d633752755ec186f4.json @@ -0,0 +1,17 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO role_permissions (id, role_id, permission_id, created_by) VALUES ($1, $2, $3, $4)", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid", + "Uuid", + "Uuid", + "Uuid" + ] + }, + "nullable": [] + }, + "hash": "fb64d4fb0ba9c699f1842b4ba59677e401c7f02c8ee45c1d633752755ec186f4" +} diff --git a/lynx/dashboard/server/.sqlx/query-fbe8bd62b7adc5ea429279fa71b9448ad856ca88e73223a1b774674cd84900c1.json b/lynx/dashboard/server/.sqlx/query-fbe8bd62b7adc5ea429279fa71b9448ad856ca88e73223a1b774674cd84900c1.json new file mode 100644 index 0000000..fa50635 --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-fbe8bd62b7adc5ea429279fa71b9448ad856ca88e73223a1b774674cd84900c1.json @@ -0,0 +1,23 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT COUNT(*) FROM user_roles ur\n JOIN role_permissions rp ON rp.role_id = ur.role_id\n JOIN permissions p ON p.id = rp.permission_id\n WHERE ur.user_id = $1 AND ur.role_id != $2 AND p.key = '*:*'", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "count", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [ + "Uuid", + "Uuid" + ] + }, + "nullable": [ + null + ] + }, + "hash": "fbe8bd62b7adc5ea429279fa71b9448ad856ca88e73223a1b774674cd84900c1" +} diff --git a/lynx/dashboard/server/.sqlx/query-fd03da9799102ed1128ada06e403c03de0878482425f648305521ff98555a388.json b/lynx/dashboard/server/.sqlx/query-fd03da9799102ed1128ada06e403c03de0878482425f648305521ff98555a388.json new file mode 100644 index 0000000..0344d6e --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-fd03da9799102ed1128ada06e403c03de0878482425f648305521ff98555a388.json @@ -0,0 +1,44 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT id, wg_ip::text AS wg_ip, api_port, status, COALESCE(arch, 'x86_64') AS arch FROM agents WHERE status != 'lockdown'", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "wg_ip", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "api_port", + "type_info": "Int4" + }, + { + "ordinal": 3, + "name": "status", + "type_info": "Text" + }, + { + "ordinal": 4, + "name": "arch", + "type_info": "Text" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + false, + false, + false, + false, + null + ] + }, + "hash": "fd03da9799102ed1128ada06e403c03de0878482425f648305521ff98555a388" +} diff --git a/lynx/dashboard/server/.sqlx/query-fdf9e1bc5d284fe3b8e73b2a3320ffd7fa624df6ce92b8ba06fcd520dc6bb995.json b/lynx/dashboard/server/.sqlx/query-fdf9e1bc5d284fe3b8e73b2a3320ffd7fa624df6ce92b8ba06fcd520dc6bb995.json new file mode 100644 index 0000000..8793176 --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-fdf9e1bc5d284fe3b8e73b2a3320ffd7fa624df6ce92b8ba06fcd520dc6bb995.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT force_password_change FROM users WHERE id = $1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "force_password_change", + "type_info": "Bool" + } + ], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [ + false + ] + }, + "hash": "fdf9e1bc5d284fe3b8e73b2a3320ffd7fa624df6ce92b8ba06fcd520dc6bb995" +} diff --git a/lynx/dashboard/server/.sqlx/query-fe6450e0a791e5e0eff0c3811fdb3f08d549a9bda036725b9ab8dea687e8eaca.json b/lynx/dashboard/server/.sqlx/query-fe6450e0a791e5e0eff0c3811fdb3f08d549a9bda036725b9ab8dea687e8eaca.json new file mode 100644 index 0000000..5fa40b1 --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-fe6450e0a791e5e0eff0c3811fdb3f08d549a9bda036725b9ab8dea687e8eaca.json @@ -0,0 +1,14 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO rotation_log (id, triggered_by, reason, scope) VALUES ($1, NULL, 'scheduled', 'all')", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [] + }, + "hash": "fe6450e0a791e5e0eff0c3811fdb3f08d549a9bda036725b9ab8dea687e8eaca" +} diff --git a/lynx/dashboard/server/.sqlx/query-feca6f89a63355b01eeafff058ded82493ce8634ab9cb2bf82e3597e349caca2.json b/lynx/dashboard/server/.sqlx/query-feca6f89a63355b01eeafff058ded82493ce8634ab9cb2bf82e3597e349caca2.json new file mode 100644 index 0000000..9a655da --- /dev/null +++ b/lynx/dashboard/server/.sqlx/query-feca6f89a63355b01eeafff058ded82493ce8634ab9cb2bf82e3597e349caca2.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT id FROM agents WHERE id = $1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + } + ], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [ + false + ] + }, + "hash": "feca6f89a63355b01eeafff058ded82493ce8634ab9cb2bf82e3597e349caca2" +} diff --git a/lynx/dashboard/server/migrations/015_roles_permissions.sql b/lynx/dashboard/server/migrations/015_roles_permissions.sql index f9d163f..e047ebd 100644 --- a/lynx/dashboard/server/migrations/015_roles_permissions.sql +++ b/lynx/dashboard/server/migrations/015_roles_permissions.sql @@ -40,19 +40,19 @@ CREATE INDEX idx_user_roles_role_id ON user_roles(role_id); -- Seed all permissions (fixed — never change these keys) INSERT INTO permissions (id, key) VALUES - (gen_random_uuid(), 'vps:read'), - (gen_random_uuid(), 'vps:create'), - (gen_random_uuid(), 'vps:edit'), - (gen_random_uuid(), 'vps:delete'), - (gen_random_uuid(), 'vps:*'), - (gen_random_uuid(), 'org:read'), - (gen_random_uuid(), 'org:create'), - (gen_random_uuid(), 'org:edit'), - (gen_random_uuid(), 'org:delete'), - (gen_random_uuid(), 'org:*'), - (gen_random_uuid(), 'project:read'), - (gen_random_uuid(), 'project:create'), - (gen_random_uuid(), 'project:edit'), - (gen_random_uuid(), 'project:delete'), - (gen_random_uuid(), 'project:*'), - (gen_random_uuid(), '*:*'); + (uuidv7(), 'vps:read'), + (uuidv7(), 'vps:create'), + (uuidv7(), 'vps:edit'), + (uuidv7(), 'vps:delete'), + (uuidv7(), 'vps:*'), + (uuidv7(), 'org:read'), + (uuidv7(), 'org:create'), + (uuidv7(), 'org:edit'), + (uuidv7(), 'org:delete'), + (uuidv7(), 'org:*'), + (uuidv7(), 'project:read'), + (uuidv7(), 'project:create'), + (uuidv7(), 'project:edit'), + (uuidv7(), 'project:delete'), + (uuidv7(), 'project:*'), + (uuidv7(), '*:*'); diff --git a/lynx/dashboard/server/migrations/019_security_alerts.sql b/lynx/dashboard/server/migrations/019_security_alerts.sql index b01ce85..a49234f 100644 --- a/lynx/dashboard/server/migrations/019_security_alerts.sql +++ b/lynx/dashboard/server/migrations/019_security_alerts.sql @@ -1,7 +1,7 @@ -- Security alerts — written on detection, read by frontend via WS or polling. -- All alerts are append-only; acknowledged_at marks admin has seen it. CREATE TABLE security_alerts ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + id UUID PRIMARY KEY DEFAULT uuidv7(), kind TEXT NOT NULL, -- rate_limit_hit, intercepted, nftables_divergence, etc. detail TEXT, agent_id UUID REFERENCES agents(id) ON DELETE SET NULL, diff --git a/lynx/dashboard/server/migrations/024_agents_arch.sql b/lynx/dashboard/server/migrations/024_agents_arch.sql new file mode 100644 index 0000000..61b4375 --- /dev/null +++ b/lynx/dashboard/server/migrations/024_agents_arch.sql @@ -0,0 +1 @@ +ALTER TABLE agents ADD COLUMN IF NOT EXISTS arch TEXT; diff --git a/lynx/dashboard/server/migrations/025_domain_cert_types.sql b/lynx/dashboard/server/migrations/025_domain_cert_types.sql new file mode 100644 index 0000000..88163bd --- /dev/null +++ b/lynx/dashboard/server/migrations/025_domain_cert_types.sql @@ -0,0 +1,9 @@ +-- Extend cert_type CHECK constraint to include 'cloudflare' and 'custom' types +-- that the backend already accepts but were missing from the original schema. + +ALTER TABLE domain_config + DROP CONSTRAINT IF EXISTS domain_config_cert_type_check; + +ALTER TABLE domain_config + ADD CONSTRAINT domain_config_cert_type_check + CHECK (cert_type IN ('self_signed', 'lets_encrypt', 'cloudflare', 'custom')); diff --git a/lynx/dashboard/server/migrations/026_nftables_protection.sql b/lynx/dashboard/server/migrations/026_nftables_protection.sql new file mode 100644 index 0000000..d41174c --- /dev/null +++ b/lynx/dashboard/server/migrations/026_nftables_protection.sql @@ -0,0 +1,56 @@ +-- Add port range support (for X11 6000-6063, BitTorrent 6881-6889, etc.) +ALTER TABLE nftables_rules ADD COLUMN port_end INTEGER; + +-- Add direction to distinguish input vs output chain rules +ALTER TABLE nftables_rules + ADD COLUMN direction TEXT NOT NULL DEFAULT 'input' + CHECK (direction IN ('input', 'output')); + +-- Extend kind constraint +ALTER TABLE nftables_rules DROP CONSTRAINT nftables_rules_kind_check; +ALTER TABLE nftables_rules ADD CONSTRAINT nftables_rules_kind_check CHECK ( + kind IN ( + 'allow_port', 'block_port', 'allow_ip', 'block_ip', 'rate_limit', + 'drop_invalid_state', 'tcp_flag_null', 'tcp_flag_xmas', 'tcp_flag_ack_new', + 'icmp_ping_limit', 'allow_icmp_errors', 'allow_ndp', + 'block_output_port' + ) +); + +-- Default global protection rules — input chain +INSERT INTO nftables_rules + (id, scope, kind, direction, ip_version, rate_per_min, description, priority, enabled) +VALUES + (uuidv7(), 'global', 'drop_invalid_state', 'input', 'both', NULL, 'Drop invalid connection states', 10, true), + (uuidv7(), 'global', 'tcp_flag_null', 'input', 'both', NULL, 'Drop NULL scan (no TCP flags set)', 20, true), + (uuidv7(), 'global', 'tcp_flag_xmas', 'input', 'both', NULL, 'Drop XMAS scan (FIN+PSH+URG)', 30, true), + (uuidv7(), 'global', 'tcp_flag_ack_new', 'input', 'both', NULL, 'Drop ACK on new connection', 40, true), + (uuidv7(), 'global', 'icmp_ping_limit', 'input', 'both', 3, 'Rate limit ICMP/ICMPv6 echo (3/s, burst 10)', 50, true), + (uuidv7(), 'global', 'allow_icmp_errors', 'input', 'both', NULL, 'Accept ICMP error types (PMTUD, path MTU)', 60, true), + (uuidv7(), 'global', 'allow_ndp', 'input', 'ipv6', NULL, 'Accept IPv6 Neighbor Discovery Protocol', 70, true); + +-- Default global protection rules — output chain (prevent spam relay / abuse) +INSERT INTO nftables_rules + (id, scope, kind, direction, port, port_end, protocol, ip_version, description, priority, enabled) +VALUES + (uuidv7(), 'global', 'block_output_port', 'output', 25, NULL, 'tcp', 'both', 'Block outbound SMTP (25/tcp)', 100, true), + (uuidv7(), 'global', 'block_output_port', 'output', 465, NULL, 'tcp', 'both', 'Block outbound SMTPS (465/tcp)', 101, true), + (uuidv7(), 'global', 'block_output_port', 'output', 587, NULL, 'tcp', 'both', 'Block outbound submission (587/tcp)', 102, true), + (uuidv7(), 'global', 'block_output_port', 'output', 23, NULL, 'tcp', 'both', 'Block outbound Telnet (23/tcp)', 110, true), + (uuidv7(), 'global', 'block_output_port', 'output', 20, NULL, 'tcp', 'both', 'Block outbound FTP data (20/tcp)', 120, true), + (uuidv7(), 'global', 'block_output_port', 'output', 21, NULL, 'tcp', 'both', 'Block outbound FTP control (21/tcp)', 121, true), + (uuidv7(), 'global', 'block_output_port', 'output', 137, NULL, 'udp', 'both', 'Block outbound NetBIOS NS (137/udp)', 130, true), + (uuidv7(), 'global', 'block_output_port', 'output', 138, NULL, 'udp', 'both', 'Block outbound NetBIOS DG (138/udp)', 131, true), + (uuidv7(), 'global', 'block_output_port', 'output', 139, NULL, 'tcp', 'both', 'Block outbound NetBIOS SS (139/tcp)', 132, true), + (uuidv7(), 'global', 'block_output_port', 'output', 445, NULL, 'tcp', 'both', 'Block outbound SMB (445/tcp)', 133, true), + (uuidv7(), 'global', 'block_output_port', 'output', 6667, NULL, 'tcp', 'both', 'Block outbound IRC (6667/tcp)', 140, true), + (uuidv7(), 'global', 'block_output_port', 'output', 111, NULL, 'tcp', 'both', 'Block outbound RPC (111/tcp)', 150, true), + (uuidv7(), 'global', 'block_output_port', 'output', 111, NULL, 'udp', 'both', 'Block outbound RPC (111/udp)', 151, true), + (uuidv7(), 'global', 'block_output_port', 'output', 69, NULL, 'udp', 'both', 'Block outbound TFTP (69/udp)', 160, true), + (uuidv7(), 'global', 'block_output_port', 'output', 6000, 6063, 'tcp', 'both', 'Block outbound X11 (6000-6063/tcp)', 170, true), + (uuidv7(), 'global', 'block_output_port', 'output', 1080, NULL, 'tcp', 'both', 'Block outbound SOCKS (1080/tcp)', 180, true), + (uuidv7(), 'global', 'block_output_port', 'output', 389, NULL, 'tcp', 'both', 'Block outbound LDAP (389/tcp)', 190, true), + (uuidv7(), 'global', 'block_output_port', 'output', 636, NULL, 'tcp', 'both', 'Block outbound LDAPS (636/tcp)', 191, true), + (uuidv7(), 'global', 'block_output_port', 'output', 5353, NULL, 'udp', 'both', 'Block outbound mDNS (5353/udp)', 200, true), + (uuidv7(), 'global', 'block_output_port', 'output', 6881, 6889, 'tcp', 'both', 'Block outbound BitTorrent TCP (6881-6889)', 210, true), + (uuidv7(), 'global', 'block_output_port', 'output', 6881, 6889, 'udp', 'both', 'Block outbound BitTorrent UDP (6881-6889)', 211, true); diff --git a/lynx/dashboard/server/migrations/027_fix_uuid_defaults.sql b/lynx/dashboard/server/migrations/027_fix_uuid_defaults.sql new file mode 100644 index 0000000..9853a01 --- /dev/null +++ b/lynx/dashboard/server/migrations/027_fix_uuid_defaults.sql @@ -0,0 +1,3 @@ +-- Fix UUID column defaults: gen_random_uuid() generates v4; project requires v7. +-- PostgreSQL 18 provides uuidv7() built-in. +ALTER TABLE security_alerts ALTER COLUMN id SET DEFAULT uuidv7(); diff --git a/lynx/dashboard/server/src/admin/handlers/rotation.rs b/lynx/dashboard/server/src/admin/handlers/rotation.rs index 0725a0e..552df5e 100644 --- a/lynx/dashboard/server/src/admin/handlers/rotation.rs +++ b/lynx/dashboard/server/src/admin/handlers/rotation.rs @@ -275,14 +275,17 @@ async fn rotate_agent_certs(state: &AppState) -> Result<(), AppError> { /// use the updated secret on the next backend start. pub async fn rotate_pg_app_password(state: &AppState) -> Result<(), AppError> { use rand::RngCore; + use zeroize::Zeroizing; let mut buf = [0u8; 24]; rand::rngs::OsRng.fill_bytes(&mut buf); - let new_pass: String = buf.iter().map(|b| format!("{b:02x}")).collect(); + let new_pass = Zeroizing::new(buf.iter().map(|b| format!("{b:02x}")).collect::()); + // Dollar-quoting ($$...$$) avoids any quote-based injection. + // new_pass is hex [0-9a-f] so "$$" can never appear inside it. sqlx::query(&format!( - "ALTER USER lynx_dashboard_app PASSWORD '{}'", - new_pass.replace('\'', "''") + "ALTER USER lynx_dashboard_app PASSWORD $${}$$", + &*new_pass )) .execute(&state.db) .await @@ -328,16 +331,17 @@ pub async fn rotate_pg_app_password(state: &AppState) -> Result<(), AppError> { /// so the new password is used after the next backend restart. pub async fn rotate_redis_password(state: &AppState) -> Result<(), AppError> { use rand::RngCore; + use zeroize::Zeroizing; let mut buf = [0u8; 24]; rand::rngs::OsRng.fill_bytes(&mut buf); - let new_pass: String = buf.iter().map(|b| format!("{b:02x}")).collect(); + let new_pass = Zeroizing::new(buf.iter().map(|b| format!("{b:02x}")).collect::()); let mut redis = state.redis.clone(); redis::cmd("CONFIG") .arg("SET") .arg("requirepass") - .arg(&new_pass) + .arg(&*new_pass) .query_async::<()>(&mut redis) .await .map_err(|e| AppError::Internal(anyhow::Error::from(e)))?; diff --git a/lynx/dashboard/server/src/agents/handlers/audit.rs b/lynx/dashboard/server/src/agents/handlers/audit.rs index cdd02b8..b27154c 100644 --- a/lynx/dashboard/server/src/agents/handlers/audit.rs +++ b/lynx/dashboard/server/src/agents/handlers/audit.rs @@ -40,9 +40,55 @@ pub async fn receive_audit_sync( return Ok(axum::http::StatusCode::NO_CONTENT); } + // Validate hash chain before persisting — same integrity check as WS path. + let mut expected_prev: String = sqlx::query_scalar!( + "SELECT entry_hash FROM audit_log WHERE agent_id = $1 ORDER BY created_at DESC LIMIT 1", + id + ) + .fetch_optional(&state.db) + .await? + .unwrap_or_default(); + + let mut ordered = entries.clone(); + ordered.sort_by_key(|e| e.created_at); + + for entry in &ordered { + if entry.agent_id != id { + continue; + } + if entry.previous_hash != expected_prev { + tracing::error!( + agent_id = %id, + entry_id = %entry.id, + "audit_log hash chain mismatch on HTTP sync — rejecting batch" + ); + let event_id = Uuid::now_v7(); + let _ = sqlx::query!( + "INSERT INTO agent_events (id, agent_id, event, detail) VALUES ($1, $2, 'audit_integrity_failure', $3)", + event_id, + id, + Some(format!("hash chain broken at entry {}", entry.id)) + ) + .execute(&state.db) + .await; + crate::alerts::fire( + &state, + "audit_integrity_failure", + Some(format!( + "agent={id} entry={} hash chain mismatch (HTTP sync)", + entry.id + )), + None::, + ) + .await; + return Err(AppError::Validation("audit hash chain mismatch".into())); + } + expected_prev = entry.entry_hash.clone(); + } + let mut tx = state.db.begin().await?; - for entry in &entries { + for entry in &ordered { if entry.agent_id != id { continue; } diff --git a/lynx/dashboard/server/src/agents/handlers/crud.rs b/lynx/dashboard/server/src/agents/handlers/crud.rs index 9a1fc2a..5c546b8 100644 --- a/lynx/dashboard/server/src/agents/handlers/crud.rs +++ b/lynx/dashboard/server/src/agents/handlers/crud.rs @@ -77,13 +77,11 @@ pub async fn register_agent( } }; - if let Err(e) = wg::add_peer( - &req.wg_pubkey, - wg_ip - .parse() - .unwrap_or(std::net::IpAddr::V4(std::net::Ipv4Addr::UNSPECIFIED)), - &psk, - ) { + let wg_ip_addr: std::net::IpAddr = wg_ip.parse().map_err(|e| { + AppError::Internal(anyhow::anyhow!("invalid allocated WG IP {wg_ip:?}: {e}")) + })?; + + if let Err(e) = wg::add_peer(&req.wg_pubkey, wg_ip_addr, &psk) { tracing::error!(agent_id = %req.agent_id, error = %e, "failed to add WG peer — add manually"); } diff --git a/lynx/dashboard/server/src/agents/handlers/events_ws.rs b/lynx/dashboard/server/src/agents/handlers/events_ws.rs index 7226031..f13b6c2 100644 --- a/lynx/dashboard/server/src/agents/handlers/events_ws.rs +++ b/lynx/dashboard/server/src/agents/handlers/events_ws.rs @@ -14,11 +14,29 @@ use tokio::sync::broadcast; /// Each connected admin browser receives a copy of every agent event. pub async fn frontend_events_ws( State(state): State, - Extension(_user): Extension, + Extension(user): Extension, headers: HeaderMap, ws: WebSocketUpgrade, ) -> Result { validate_ws_origin(&state, &headers).await?; + + // Require vps:read permission to receive agent events. + let has_access: bool = sqlx::query_scalar!( + r#"SELECT EXISTS( + SELECT 1 FROM user_roles ur + JOIN role_permissions rp ON rp.role_id = ur.role_id + JOIN permissions p ON p.id = rp.permission_id + WHERE ur.user_id = $1 AND p.key IN ('vps:read','vps:*','*:*') + ) AS "exists!""#, + user.user_id + ) + .fetch_one(&state.db) + .await?; + + if !has_access { + return Err(AppError::Forbidden); + } + let rx = state.events_tx.subscribe(); Ok(ws.on_upgrade(move |socket| handle_events_socket(socket, rx))) } @@ -75,8 +93,14 @@ pub(crate) async fn validate_ws_origin( let allowed = if let Some(ref domain) = configured_domain { origin == format!("https://{domain}") } else { - // No domain configured — browser reaches dashboard via https://IP:19443 - origin.starts_with("https://") + // No domain configured — browser reaches dashboard via https://IP:19443. + // Use the Host header to derive the expected origin exactly, preventing + // cross-site WebSocket hijacking from other HTTPS origins. + if let Some(host) = headers.get("host").and_then(|v| v.to_str().ok()) { + origin == format!("https://{host}") + } else { + false + } }; if !allowed { diff --git a/lynx/dashboard/server/src/agents/handlers/metrics_ws.rs b/lynx/dashboard/server/src/agents/handlers/metrics_ws.rs index 77f3dee..4c2039d 100644 --- a/lynx/dashboard/server/src/agents/handlers/metrics_ws.rs +++ b/lynx/dashboard/server/src/agents/handlers/metrics_ws.rs @@ -21,16 +21,32 @@ pub async fn frontend_metrics_ws( ) -> Result { super::events_ws::validate_ws_origin(&state, &headers).await?; - let exists = sqlx::query_scalar!("SELECT id FROM agents WHERE id = $1", agent_id) + // 404 for both "not found" and "no access" — prevents enumeration of agent IDs. + let agent_exists = sqlx::query_scalar!("SELECT id FROM agents WHERE id = $1", agent_id) .fetch_optional(&state.db) .await? .is_some(); - if !exists { + if !agent_exists { return Err(AppError::NotFound); } - let _ = user; + // Require at least vps:read permission to subscribe to any agent's metrics. + let has_access: bool = sqlx::query_scalar!( + r#"SELECT EXISTS( + SELECT 1 FROM user_roles ur + JOIN role_permissions rp ON rp.role_id = ur.role_id + JOIN permissions p ON p.id = rp.permission_id + WHERE ur.user_id = $1 AND p.key IN ('vps:read','vps:*','*:*') + ) AS "exists!""#, + user.user_id + ) + .fetch_one(&state.db) + .await?; + + if !has_access { + return Err(AppError::NotFound); + } Ok(ws.on_upgrade(move |socket| handle_frontend_socket(state, agent_id, socket))) } diff --git a/lynx/dashboard/server/src/agents/heartbeat.rs b/lynx/dashboard/server/src/agents/heartbeat.rs index 9e45e1b..815f7cc 100644 --- a/lynx/dashboard/server/src/agents/heartbeat.rs +++ b/lynx/dashboard/server/src/agents/heartbeat.rs @@ -21,7 +21,7 @@ pub async fn run_scheduler(state: AppState) { async fn poll_agents(state: &AppState) { let agents = match sqlx::query!( - "SELECT id, wg_ip::text AS wg_ip, api_port, status FROM agents WHERE status != 'lockdown'" + "SELECT id, wg_ip::text AS wg_ip, api_port, status, COALESCE(arch, 'x86_64') AS arch FROM agents WHERE status != 'lockdown'" ) .fetch_all(&state.db) .await @@ -64,9 +64,20 @@ async fn poll_agents(state: &AppState) { } let url = format!("http://{}:{}/heartbeat", agent.wg_ip, agent.api_port); + // Heartbeat ACK is a signed command so the agent can verify the dashboard's + // Ed25519 signature — bearer token alone cannot reset the lockdown timer. + let heartbeat_cmd = serde_json::json!({ "type": "agent.heartbeat_ack" }); + let signed = match cmd::sign_command_system(&state.config, id, "read", &heartbeat_cmd) { + Ok(s) => s, + Err(e) => { + tracing::warn!(agent_id = %id, "heartbeat: sign_command failed: {e}"); + continue; + } + }; let resp = client .post(&url) .header("Authorization", format!("Bearer {token}")) + .json(&signed) .send() .await; @@ -124,7 +135,9 @@ async fn poll_agents(state: &AppState) { if let Some(ref current) = reported_version { if let Some(ref target) = latest { if current != target { - dispatch_update(state, id, &agent.wg_ip, agent.api_port, target).await; + let arch = agent.arch.as_deref().unwrap_or("x86_64"); + dispatch_update(state, id, &agent.wg_ip, agent.api_port, target, arch) + .await; } } } @@ -133,12 +146,22 @@ async fn poll_agents(state: &AppState) { } async fn dispatch_update_ws(state: &AppState, agent_id: Uuid, version: &str) { + let arch = sqlx::query_scalar!( + "SELECT COALESCE(arch, 'x86_64') FROM agents WHERE id = $1", + agent_id + ) + .fetch_optional(&state.db) + .await + .ok() + .flatten() + .flatten() + .unwrap_or_else(|| "x86_64".to_string()); let github_repo = "Jaro-c/Lynx"; let download_url = format!( - "https://github.com/{github_repo}/releases/download/agent@{version}/lynx-agent-linux-x86_64" + "https://github.com/{github_repo}/releases/download/agent@{version}/lynx-agent-linux-{arch}" ); let sig_url = format!( - "https://github.com/{github_repo}/releases/download/agent@{version}/lynx-agent-linux-x86_64.sig" + "https://github.com/{github_repo}/releases/download/agent@{version}/lynx-agent-linux-{arch}.sig" ); let command = serde_json::json!({ "type": "update.self", @@ -170,13 +193,14 @@ async fn dispatch_update( wg_ip: &str, api_port: i32, version: &str, + arch: &str, ) { let github_repo = "Jaro-c/Lynx"; let download_url = format!( - "https://github.com/{github_repo}/releases/download/agent@{version}/lynx-agent-linux-x86_64" + "https://github.com/{github_repo}/releases/download/agent@{version}/lynx-agent-linux-{arch}" ); let sig_url = format!( - "https://github.com/{github_repo}/releases/download/agent@{version}/lynx-agent-linux-x86_64.sig" + "https://github.com/{github_repo}/releases/download/agent@{version}/lynx-agent-linux-{arch}.sig" ); let command = serde_json::json!({ "type": "update.self", diff --git a/lynx/dashboard/server/src/agents/ws_hub.rs b/lynx/dashboard/server/src/agents/ws_hub.rs index e173e1a..1dc0a60 100644 --- a/lynx/dashboard/server/src/agents/ws_hub.rs +++ b/lynx/dashboard/server/src/agents/ws_hub.rs @@ -181,10 +181,16 @@ async fn handle_agent_message( .get("version") .and_then(|v| v.as_str()) .map(|s| s.to_string()); + let arch = msg + .data + .get("arch") + .and_then(|v| v.as_str()) + .map(|s| s.to_string()); sqlx::query!( - "UPDATE agents SET status=$1, last_heartbeat=NOW(), version=$2 WHERE id=$3", + "UPDATE agents SET status=$1, last_heartbeat=NOW(), version=$2, arch=$3 WHERE id=$4", status, version, + arch, agent_id ) .execute(&state.db) @@ -368,6 +374,7 @@ pub async fn is_connected(state: &AppState, agent_id: Uuid) -> bool { /// Push current global rules to an agent that has pending unsynced entries. /// Called on WS connect — catches up agents that were offline during a global push. +/// Sends both input chain (lynx-global) and output chain (lynx-global-output). async fn push_pending_global_sync(state: &AppState, agent_id: Uuid) { let has_pending = sqlx::query_scalar!( "SELECT 1 FROM global_rule_sync WHERE agent_id = $1 AND synced_at IS NULL LIMIT 1", @@ -383,12 +390,16 @@ async fn push_pending_global_sync(state: &AppState, agent_id: Uuid) { return; } - // Generate current global chain body from all enabled global rules. - let rules = match sqlx::query!( - r#"SELECT kind, port, protocol, ip_list, rate_per_min, priority - FROM nftables_rules - WHERE scope = 'global' AND enabled = true - ORDER BY priority ASC, created_at ASC"# + let rules = match sqlx::query_as!( + crate::nftables::NftRule, + r#" + SELECT id, scope, agent_id, kind, port, port_end, protocol, + ip_list, ip_version, rate_per_min, description, + priority, enabled, direction, created_by, created_at, updated_at + FROM nftables_rules + WHERE scope = 'global' AND enabled = true + ORDER BY priority ASC, created_at ASC + "# ) .fetch_all(&state.db) .await @@ -400,40 +411,37 @@ async fn push_pending_global_sync(state: &AppState, agent_id: Uuid) { } }; - // Convert DB rows to nft chain body text. - let body = rules - .iter() - .map(|r| { - crate::nftables::rule_line( - &r.kind, - r.port.map(|p| p as u16), - r.protocol.as_deref(), - &r.ip_list, - r.rate_per_min.map(|r| r as u32), - ) - }) - .collect::>() - .join("\n"); - - let signed = match crate::crypto::cmd::sign_command_system( - &state.config, - agent_id, - "write", - &serde_json::json!({ - "type": "nftables.apply", - "chain": "lynx-global", - "rules": body, - }), - ) { - Ok(s) => s, - Err(e) => { - tracing::warn!(agent_id = %agent_id, error = %e, "pending_sync: sign failed"); - return; - } + let input_body = crate::nftables::rules_to_nft_chain(&rules); + let output_body = crate::nftables::rules_to_nft_output_chain(&rules); + + let sign = |chain: &str, body: &str| { + crate::crypto::cmd::sign_command_system( + &state.config, + agent_id, + "write", + &serde_json::json!({ + "type": "nftables.apply", + "chain": chain, + "rules": body, + }), + ) + }; + + let (Ok(signed_in), Ok(signed_out)) = ( + sign("lynx-global", &input_body), + sign("lynx-global-output", &output_body), + ) else { + tracing::warn!(agent_id = %agent_id, "pending_sync: sign failed"); + return; }; - let signed_val = serde_json::to_value(&signed).unwrap_or_default(); - if push_command(state, agent_id, signed_val).await.is_some() { + let in_val = serde_json::to_value(&signed_in).unwrap_or_default(); + let out_val = serde_json::to_value(&signed_out).unwrap_or_default(); + + let sent_in = push_command(state, agent_id, in_val).await.is_some(); + let sent_out = push_command(state, agent_id, out_val).await.is_some(); + + if sent_in && sent_out { let _ = sqlx::query!( "UPDATE global_rule_sync SET synced_at = NOW() WHERE agent_id = $1 AND synced_at IS NULL", agent_id diff --git a/lynx/dashboard/server/src/auth/handlers/login.rs b/lynx/dashboard/server/src/auth/handlers/login.rs index 880fd67..bd09d6e 100644 --- a/lynx/dashboard/server/src/auth/handlers/login.rs +++ b/lynx/dashboard/server/src/auth/handlers/login.rs @@ -58,6 +58,9 @@ pub async fn login( Some(u) => u, }; + // Per-username rate limit — prevents credential stuffing across many IPs. + rate_limit::check_login_username(&mut redis, &username).await?; + let ok = password::verify(&body.password, &u.password_hash)?; if !ok { return Err(AppError::InvalidCredentials); diff --git a/lynx/dashboard/server/src/auth/handlers/register.rs b/lynx/dashboard/server/src/auth/handlers/register.rs index 97481e6..30a56e0 100644 --- a/lynx/dashboard/server/src/auth/handlers/register.rs +++ b/lynx/dashboard/server/src/auth/handlers/register.rs @@ -58,11 +58,12 @@ pub async fn register( .map(|s| s.as_bytes()) .unwrap_or(b""); - let token_ok: bool = if provided.len() == expected.len() && !expected.is_empty() { - provided.ct_eq(expected).into() - } else { - false - }; + // Hash both to fixed length before comparing so neither branch nor + // length difference leaks timing info. Always executes both digests. + use sha2::{Digest, Sha256}; + let h_provided = Sha256::digest(provided); + let h_expected = Sha256::digest(expected); + let token_ok: bool = (!expected.is_empty()) & bool::from(h_provided.ct_eq(&h_expected)); if !token_ok { password::zeroize_str(&mut body.password); diff --git a/lynx/dashboard/server/src/auth/middleware.rs b/lynx/dashboard/server/src/auth/middleware.rs index 3a5fe77..07ae274 100644 --- a/lynx/dashboard/server/src/auth/middleware.rs +++ b/lynx/dashboard/server/src/auth/middleware.rs @@ -45,7 +45,20 @@ pub async fn require_auth( let expected_ip = crypto::hash::ip_hash(&client_ip); let expected_ua = crypto::hash::ua_hash(&client_ua); - if claims.ip_hash != expected_ip || claims.ua_hash != expected_ua { + // Constant-time comparison — prevents timing side-channels that could reveal + // which hash (IP vs UA) mismatched. Bitwise OR avoids short-circuit evaluation. + use subtle::ConstantTimeEq; + let ip_ok: bool = claims + .ip_hash + .as_bytes() + .ct_eq(expected_ip.as_bytes()) + .into(); + let ua_ok: bool = claims + .ua_hash + .as_bytes() + .ct_eq(expected_ua.as_bytes()) + .into(); + if !ip_ok | !ua_ok { let _ = crate::auth::session::revoke_access_jti(&mut redis, claims.jti).await; let _ = crate::auth::session::log_event(&state.db, claims.session_id, "intercepted").await; let _ = crate::auth::session::delete_by_session_id(&state.db, claims.session_id).await; diff --git a/lynx/dashboard/server/src/auth/rate_limit.rs b/lynx/dashboard/server/src/auth/rate_limit.rs index 0a40269..6982696 100644 --- a/lynx/dashboard/server/src/auth/rate_limit.rs +++ b/lynx/dashboard/server/src/auth/rate_limit.rs @@ -3,6 +3,8 @@ use redis::{aio::ConnectionManager, AsyncCommands}; const LOGIN_LIMIT: i64 = 5; const LOGIN_WINDOW: i64 = 900; +// Per-username limit: 10 attempts across all IPs per 15 min — stops credential stuffing. +const LOGIN_USER_LIMIT: i64 = 10; const REGISTER_LIMIT: i64 = 3; const REGISTER_WINDOW: i64 = 3600; const REFRESH_LIMIT: i64 = 10; @@ -12,6 +14,17 @@ pub async fn check_login(redis: &mut ConnectionManager, ip: &str) -> Result<()> check(redis, &format!("rl:login:{ip}"), LOGIN_LIMIT, LOGIN_WINDOW).await } +/// Called after confirming the username exists, to rate-limit per username. +pub async fn check_login_username(redis: &mut ConnectionManager, username: &str) -> Result<()> { + check( + redis, + &format!("rl:login:u:{username}"), + LOGIN_USER_LIMIT, + LOGIN_WINDOW, + ) + .await +} + pub async fn check_register(redis: &mut ConnectionManager, ip: &str) -> Result<()> { check( redis, diff --git a/lynx/dashboard/server/src/branding/handlers.rs b/lynx/dashboard/server/src/branding/handlers.rs index 92cce42..fd45541 100644 --- a/lynx/dashboard/server/src/branding/handlers.rs +++ b/lynx/dashboard/server/src/branding/handlers.rs @@ -33,6 +33,29 @@ pub async fn update_branding( State(state): State, Json(req): Json, ) -> Result { + // Validate company name length. + if let Some(ref name) = req.company_name { + if name.len() > 255 { + return Err(AppError::Validation( + "company_name must be ≤ 255 characters".into(), + )); + } + } + + // Validate logo_url: must be https:// and not excessively long. + if let Some(ref url) = req.logo_url { + if !url.starts_with("https://") { + return Err(AppError::Validation( + "logo_url must start with https://".into(), + )); + } + if url.len() > 2048 { + return Err(AppError::Validation( + "logo_url must be ≤ 2048 characters".into(), + )); + } + } + // Validate hex colors if provided for (field, val) in [ ("primary_color", &req.primary_color), diff --git a/lynx/dashboard/server/src/crypto/jwt.rs b/lynx/dashboard/server/src/crypto/jwt.rs index 12cd17b..1c76a7b 100644 --- a/lynx/dashboard/server/src/crypto/jwt.rs +++ b/lynx/dashboard/server/src/crypto/jwt.rs @@ -128,10 +128,15 @@ fn validate_claims(c: &serde_json::Value) -> Result<()> { if exp <= now { anyhow::bail!("token expired"); } - if let Some(nbf) = c["nbf"].as_u64() { - if nbf > now { - anyhow::bail!("token not yet valid"); - } + // nbf required — reject tokens that were never issued with this claim. + let nbf = c["nbf"].as_u64().context("missing nbf")?; + if nbf > now { + anyhow::bail!("token not yet valid"); + } + // iat required — must not be in the future (clock skew tolerance: 5s). + let iat = c["iat"].as_u64().context("missing iat")?; + if iat > now + 5 { + anyhow::bail!("token iat in the future"); } Ok(()) } diff --git a/lynx/dashboard/server/src/domain/handlers/api.rs b/lynx/dashboard/server/src/domain/handlers/api.rs index 13bad43..e6a804e 100644 --- a/lynx/dashboard/server/src/domain/handlers/api.rs +++ b/lynx/dashboard/server/src/domain/handlers/api.rs @@ -82,7 +82,17 @@ pub async fn set_domain( ) -> Result { let domain = req.domain.trim().to_lowercase(); - if domain.is_empty() || domain.contains(' ') { + // Strict validation: only DNS-valid characters (alphanumeric, hyphens, dots). + // Rejects newlines, semicolons, slashes, etc. that could inject nginx directives. + let domain_valid = !domain.is_empty() + && domain.len() <= 253 + && domain + .chars() + .all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '.') + && !domain.starts_with('.') + && !domain.ends_with('.') + && !domain.contains(".."); + if !domain_valid { return Err(AppError::Validation("invalid domain".into())); } diff --git a/lynx/dashboard/server/src/lib.rs b/lynx/dashboard/server/src/lib.rs index 0149a29..853dd93 100644 --- a/lynx/dashboard/server/src/lib.rs +++ b/lynx/dashboard/server/src/lib.rs @@ -66,6 +66,10 @@ pub fn build_router(state: AppState) -> Router { header::HeaderName::from_static("referrer-policy"), HeaderValue::from_static("no-referrer"), )) + .layer(SetResponseHeaderLayer::if_not_present( + header::HeaderName::from_static("strict-transport-security"), + HeaderValue::from_static("max-age=63072000; includeSubDomains"), + )) } async fn health() -> impl IntoResponse { diff --git a/lynx/dashboard/server/src/nftables/handlers/global.rs b/lynx/dashboard/server/src/nftables/handlers/global.rs index d1f3be9..45c12f1 100644 --- a/lynx/dashboard/server/src/nftables/handlers/global.rs +++ b/lynx/dashboard/server/src/nftables/handlers/global.rs @@ -3,7 +3,7 @@ use crate::{ auth::middleware::AuthUser, crypto::cmd::sign_command, error::AppError, - nftables::{rules_to_nft_chain, CreateRuleRequest, NftRule}, + nftables::{rules_to_nft_chain, rules_to_nft_output_chain, CreateRuleRequest, NftRule}, state::AppState, }; use axum::{ @@ -20,9 +20,9 @@ pub async fn list_global_rules( let rules = sqlx::query_as!( NftRule, r#" - SELECT id, scope, agent_id, kind, port, protocol, + SELECT id, scope, agent_id, kind, port, port_end, protocol, ip_list, ip_version, rate_per_min, description, - priority, enabled, created_by, created_at, updated_at + priority, enabled, direction, created_by, created_at, updated_at FROM nftables_rules WHERE scope = 'global' ORDER BY priority ASC, created_at ASC @@ -45,27 +45,30 @@ pub async fn create_global_rule( let ip_list = req.ip_list.unwrap_or_default(); let ip_version = req.ip_version.unwrap_or_else(|| "both".into()); let priority = req.priority.unwrap_or(0); + let direction = req.direction.unwrap_or_else(|| "input".into()); let rule = sqlx::query_as!( NftRule, r#" INSERT INTO nftables_rules - (id, scope, agent_id, kind, port, protocol, ip_list, ip_version, - rate_per_min, description, priority, created_by) - VALUES ($1, 'global', NULL, $2, $3, $4, $5, $6, $7, $8, $9, $10) - RETURNING id, scope, agent_id, kind, port, protocol, + (id, scope, agent_id, kind, port, port_end, protocol, ip_list, ip_version, + rate_per_min, description, priority, direction, created_by) + VALUES ($1, 'global', NULL, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) + RETURNING id, scope, agent_id, kind, port, port_end, protocol, ip_list, ip_version, rate_per_min, description, - priority, enabled, created_by, created_at, updated_at + priority, enabled, direction, created_by, created_at, updated_at "#, id, req.kind, req.port, + req.port_end, req.protocol, &ip_list, ip_version, req.rate_per_min, req.description, priority, + direction, user.user_id, ) .fetch_one(&state.db) @@ -93,6 +96,7 @@ pub async fn delete_global_rule( } /// Push current global rules to all online agents. +/// Sends two commands per agent: input chain body + output chain body. pub async fn push_global_rules( State(state): State, Extension(user): Extension, @@ -100,9 +104,9 @@ pub async fn push_global_rules( let rules = sqlx::query_as!( NftRule, r#" - SELECT id, scope, agent_id, kind, port, protocol, + SELECT id, scope, agent_id, kind, port, port_end, protocol, ip_list, ip_version, rate_per_min, description, - priority, enabled, created_by, created_at, updated_at + priority, enabled, direction, created_by, created_at, updated_at FROM nftables_rules WHERE scope = 'global' AND enabled = true ORDER BY priority ASC @@ -111,7 +115,8 @@ pub async fn push_global_rules( .fetch_all(&state.db) .await?; - let chain_body = rules_to_nft_chain(&rules); + let input_body = rules_to_nft_chain(&rules); + let output_body = rules_to_nft_output_chain(&rules); let agents = sqlx::query!("SELECT id, wg_ip, api_port, status FROM agents WHERE status = 'online'") @@ -122,38 +127,16 @@ pub async fn push_global_rules( let mut failed = 0u32; for agent in &agents { - let command = json!({ - "type": "nftables.apply", - "chain": "lynx-global", - "rules": chain_body, - }); - - let Ok(signed) = sign_command(&state.config, agent.id, user.user_id, "admin", &command) - else { - failed += 1; - continue; - }; - - let signed_val = serde_json::to_value(&signed).unwrap_or_default(); - let sent = if ws_hub::push_command(&state, agent.id, signed_val) - .await - .is_some() - { - true - } else { - // HTTP fallback - let url = format!("http://{}:{}/cmd", agent.wg_ip, agent.api_port); - let tok = &*state.config.internal_token; - reqwest::Client::new() - .post(&url) - .header("Authorization", format!("Bearer {tok}")) - .json(&signed) - .timeout(std::time::Duration::from_secs(15)) - .send() - .await - .map(|r| r.status().is_success()) - .unwrap_or(false) - }; + let sent = push_both_chains( + &state, + agent.id, + agent.wg_ip.as_str(), + agent.api_port, + user.user_id, + &input_body, + &output_body, + ) + .await; if sent { for rule in &rules { @@ -202,6 +185,92 @@ pub async fn push_global_rules( )) } +/// Send the global input and output chain bodies to a single agent. +/// Returns true only if both commands succeed. +async fn push_both_chains( + state: &AppState, + agent_id: Uuid, + wg_ip: &str, + api_port: i32, + user_id: Uuid, + input_body: &str, + output_body: &str, +) -> bool { + let input_cmd = json!({ + "type": "nftables.apply", + "chain": "lynx-global", + "rules": input_body, + }); + let output_cmd = json!({ + "type": "nftables.apply", + "chain": "lynx-global-output", + "rules": output_body, + }); + + let Ok(signed_in) = sign_command( + state.config.as_ref(), + agent_id, + user_id, + "admin", + &input_cmd, + ) else { + return false; + }; + let Ok(signed_out) = sign_command( + state.config.as_ref(), + agent_id, + user_id, + "admin", + &output_cmd, + ) else { + return false; + }; + + let in_val = serde_json::to_value(&signed_in).unwrap_or_default(); + let sent_in = if ws_hub::push_command(state, agent_id, in_val) + .await + .is_some() + { + true + } else { + let url = format!("http://{wg_ip}:{api_port}/cmd"); + let tok = &*state.config.internal_token; + reqwest::Client::new() + .post(&url) + .header("Authorization", format!("Bearer {tok}")) + .json(&signed_in) + .timeout(std::time::Duration::from_secs(15)) + .send() + .await + .map(|r| r.status().is_success()) + .unwrap_or(false) + }; + + if !sent_in { + return false; + } + + let out_val = serde_json::to_value(&signed_out).unwrap_or_default(); + if ws_hub::push_command(state, agent_id, out_val) + .await + .is_some() + { + true + } else { + let url = format!("http://{wg_ip}:{api_port}/cmd"); + let tok = &*state.config.internal_token; + reqwest::Client::new() + .post(&url) + .header("Authorization", format!("Bearer {tok}")) + .json(&signed_out) + .timeout(std::time::Duration::from_secs(15)) + .send() + .await + .map(|r| r.status().is_success()) + .unwrap_or(false) + } +} + fn validate_rule_request(req: &CreateRuleRequest) -> Result<(), AppError> { let valid_kinds = [ "allow_port", @@ -209,13 +278,21 @@ fn validate_rule_request(req: &CreateRuleRequest) -> Result<(), AppError> { "allow_ip", "block_ip", "rate_limit", + "drop_invalid_state", + "tcp_flag_null", + "tcp_flag_xmas", + "tcp_flag_ack_new", + "icmp_ping_limit", + "allow_icmp_errors", + "allow_ndp", + "block_output_port", ]; if !valid_kinds.contains(&req.kind.as_str()) { return Err(AppError::Validation("invalid rule kind".into())); } if matches!( req.kind.as_str(), - "allow_port" | "block_port" | "rate_limit" + "allow_port" | "block_port" | "rate_limit" | "block_output_port" ) && req.port.is_none() { return Err(AppError::Validation( @@ -237,5 +314,51 @@ fn validate_rule_request(req: &CreateRuleRequest) -> Result<(), AppError> { return Err(AppError::Validation("invalid ip_version".into())); } } + if let Some(dir) = &req.direction { + if !["input", "output"].contains(&dir.as_str()) { + return Err(AppError::Validation("invalid direction".into())); + } + } + // Validate port range: 1-65535, start ≤ end. + if let Some(p) = req.port { + if !(1..=65535).contains(&p) { + return Err(AppError::Validation("port must be 1–65535".into())); + } + } + if let Some(pe) = req.port_end { + if !(1..=65535).contains(&pe) { + return Err(AppError::Validation("port_end must be 1–65535".into())); + } + if let Some(p) = req.port { + if p > pe { + return Err(AppError::Validation("port must be ≤ port_end".into())); + } + } + } + // Validate IP list entries are valid IPs or CIDRs (IP/prefix_len). + if let Some(ref ips) = req.ip_list { + for ip_str in ips { + if !is_valid_ip_or_cidr(ip_str) { + return Err(AppError::Validation(format!( + "invalid IP or CIDR: {ip_str}" + ))); + } + } + } Ok(()) } + +fn is_valid_ip_or_cidr(s: &str) -> bool { + if s.parse::().is_ok() { + return true; + } + if let Some((ip_part, prefix_part)) = s.rsplit_once('/') { + if let Ok(ip) = ip_part.parse::() { + if let Ok(prefix) = prefix_part.parse::() { + let max = if ip.is_ipv4() { 32u8 } else { 128u8 }; + return prefix <= max; + } + } + } + false +} diff --git a/lynx/dashboard/server/src/nftables/handlers/local.rs b/lynx/dashboard/server/src/nftables/handlers/local.rs index 7b47d12..a3de659 100644 --- a/lynx/dashboard/server/src/nftables/handlers/local.rs +++ b/lynx/dashboard/server/src/nftables/handlers/local.rs @@ -3,7 +3,7 @@ use crate::{ auth::middleware::AuthUser, crypto::cmd::sign_command, error::AppError, - nftables::{rules_to_nft_chain, CreateRuleRequest, NftRule}, + nftables::{rules_to_nft_chain, rules_to_nft_output_chain, CreateRuleRequest, NftRule}, state::AppState, }; use axum::{ @@ -18,7 +18,6 @@ pub async fn list_local_rules( State(state): State, Path(agent_id): Path, ) -> Result { - // Verify agent exists. let exists = sqlx::query!("SELECT id FROM agents WHERE id = $1", agent_id) .fetch_optional(&state.db) .await?; @@ -29,9 +28,9 @@ pub async fn list_local_rules( let rules = sqlx::query_as!( NftRule, r#" - SELECT id, scope, agent_id, kind, port, protocol, + SELECT id, scope, agent_id, kind, port, port_end, protocol, ip_list, ip_version, rate_per_min, description, - priority, enabled, created_by, created_at, updated_at + priority, enabled, direction, created_by, created_at, updated_at FROM nftables_rules WHERE scope = 'local' AND agent_id = $1 ORDER BY priority ASC, created_at ASC @@ -63,28 +62,31 @@ pub async fn create_local_rule( let ip_list = req.ip_list.unwrap_or_default(); let ip_version = req.ip_version.unwrap_or_else(|| "both".into()); let priority = req.priority.unwrap_or(0); + let direction = req.direction.unwrap_or_else(|| "input".into()); let rule = sqlx::query_as!( NftRule, r#" INSERT INTO nftables_rules - (id, scope, agent_id, kind, port, protocol, ip_list, ip_version, - rate_per_min, description, priority, created_by) - VALUES ($1, 'local', $2, $3, $4, $5, $6, $7, $8, $9, $10, $11) - RETURNING id, scope, agent_id, kind, port, protocol, + (id, scope, agent_id, kind, port, port_end, protocol, ip_list, ip_version, + rate_per_min, description, priority, direction, created_by) + VALUES ($1, 'local', $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13) + RETURNING id, scope, agent_id, kind, port, port_end, protocol, ip_list, ip_version, rate_per_min, description, - priority, enabled, created_by, created_at, updated_at + priority, enabled, direction, created_by, created_at, updated_at "#, id, agent_id, req.kind, req.port, + req.port_end, req.protocol, &ip_list, ip_version, req.rate_per_min, req.description, priority, + direction, user.user_id, ) .fetch_one(&state.db) @@ -112,7 +114,7 @@ pub async fn delete_local_rule( Ok(axum::http::StatusCode::NO_CONTENT) } -/// Push local rules to the specific agent. +/// Push local rules to the specific agent (input + output chains). pub async fn push_local_rules( State(state): State, Extension(user): Extension, @@ -133,9 +135,9 @@ pub async fn push_local_rules( let rules = sqlx::query_as!( NftRule, r#" - SELECT id, scope, agent_id, kind, port, protocol, + SELECT id, scope, agent_id, kind, port, port_end, protocol, ip_list, ip_version, rate_per_min, description, - priority, enabled, created_by, created_at, updated_at + priority, enabled, direction, created_by, created_at, updated_at FROM nftables_rules WHERE scope = 'local' AND agent_id = $1 AND enabled = true ORDER BY priority ASC @@ -145,34 +147,62 @@ pub async fn push_local_rules( .fetch_all(&state.db) .await?; - let chain_body = rules_to_nft_chain(&rules); + let input_body = rules_to_nft_chain(&rules); + let output_body = rules_to_nft_output_chain(&rules); - let command = json!({ + let input_cmd = json!({ "type": "nftables.apply", "chain": "lynx-local", - "rules": chain_body, + "rules": input_body, }); + let output_cmd = json!({ + "type": "nftables.apply", + "chain": "lynx-local-output", + "rules": output_body, + }); + + let signed_in = sign_command(&state.config, agent.id, user.user_id, "admin", &input_cmd)?; + let signed_out = sign_command(&state.config, agent.id, user.user_id, "admin", &output_cmd)?; - let signed = sign_command(&state.config, agent.id, user.user_id, "admin", &command)?; - let signed_val = serde_json::to_value(&signed).unwrap_or_default(); - - let ok = if let Some(body) = ws_hub::push_command(&state, agent.id, signed_val).await { - body.get("ok").and_then(|v| v.as_bool()).unwrap_or(false) - } else { - let url = format!("http://{}:{}/cmd", agent.wg_ip, agent.api_port); - let tok = &*state.config.internal_token; - reqwest::Client::new() - .post(&url) - .header("Authorization", format!("Bearer {tok}")) - .json(&signed) - .timeout(std::time::Duration::from_secs(15)) - .send() - .await - .map(|r| r.status().is_success()) - .unwrap_or(false) + let ok_in = { + let v = serde_json::to_value(&signed_in).unwrap_or_default(); + if let Some(body) = ws_hub::push_command(&state, agent.id, v).await { + body.get("ok").and_then(|v| v.as_bool()).unwrap_or(false) + } else { + let url = format!("http://{}:{}/cmd", agent.wg_ip, agent.api_port); + let tok = &*state.config.internal_token; + reqwest::Client::new() + .post(&url) + .header("Authorization", format!("Bearer {tok}")) + .json(&signed_in) + .timeout(std::time::Duration::from_secs(15)) + .send() + .await + .map(|r| r.status().is_success()) + .unwrap_or(false) + } }; - Ok(Json(json!({ "ok": ok }))) + let ok_out = { + let v = serde_json::to_value(&signed_out).unwrap_or_default(); + if let Some(body) = ws_hub::push_command(&state, agent.id, v).await { + body.get("ok").and_then(|v| v.as_bool()).unwrap_or(false) + } else { + let url = format!("http://{}:{}/cmd", agent.wg_ip, agent.api_port); + let tok = &*state.config.internal_token; + reqwest::Client::new() + .post(&url) + .header("Authorization", format!("Bearer {tok}")) + .json(&signed_out) + .timeout(std::time::Duration::from_secs(15)) + .send() + .await + .map(|r| r.status().is_success()) + .unwrap_or(false) + } + }; + + Ok(Json(json!({ "ok": ok_in && ok_out }))) } fn validate_rule_request(req: &CreateRuleRequest) -> Result<(), AppError> { @@ -182,13 +212,21 @@ fn validate_rule_request(req: &CreateRuleRequest) -> Result<(), AppError> { "allow_ip", "block_ip", "rate_limit", + "drop_invalid_state", + "tcp_flag_null", + "tcp_flag_xmas", + "tcp_flag_ack_new", + "icmp_ping_limit", + "allow_icmp_errors", + "allow_ndp", + "block_output_port", ]; if !valid_kinds.contains(&req.kind.as_str()) { return Err(AppError::Validation("invalid rule kind".into())); } if matches!( req.kind.as_str(), - "allow_port" | "block_port" | "rate_limit" + "allow_port" | "block_port" | "rate_limit" | "block_output_port" ) && req.port.is_none() { return Err(AppError::Validation( @@ -210,5 +248,49 @@ fn validate_rule_request(req: &CreateRuleRequest) -> Result<(), AppError> { return Err(AppError::Validation("invalid ip_version".into())); } } + if let Some(dir) = &req.direction { + if !["input", "output"].contains(&dir.as_str()) { + return Err(AppError::Validation("invalid direction".into())); + } + } + if let Some(p) = req.port { + if !(1..=65535).contains(&p) { + return Err(AppError::Validation("port must be 1–65535".into())); + } + } + if let Some(pe) = req.port_end { + if !(1..=65535).contains(&pe) { + return Err(AppError::Validation("port_end must be 1–65535".into())); + } + if let Some(p) = req.port { + if p > pe { + return Err(AppError::Validation("port must be ≤ port_end".into())); + } + } + } + if let Some(ref ips) = req.ip_list { + for ip_str in ips { + if !is_valid_ip_or_cidr(ip_str) { + return Err(AppError::Validation(format!( + "invalid IP or CIDR: {ip_str}" + ))); + } + } + } Ok(()) } + +fn is_valid_ip_or_cidr(s: &str) -> bool { + if s.parse::().is_ok() { + return true; + } + if let Some((ip_part, prefix_part)) = s.rsplit_once('/') { + if let Ok(ip) = ip_part.parse::() { + if let Ok(prefix) = prefix_part.parse::() { + let max = if ip.is_ipv4() { 32u8 } else { 128u8 }; + return prefix <= max; + } + } + } + false +} diff --git a/lynx/dashboard/server/src/nftables/mod.rs b/lynx/dashboard/server/src/nftables/mod.rs index 72ff6f4..e75cc71 100644 --- a/lynx/dashboard/server/src/nftables/mod.rs +++ b/lynx/dashboard/server/src/nftables/mod.rs @@ -12,6 +12,7 @@ pub struct NftRule { pub agent_id: Option, pub kind: String, pub port: Option, + pub port_end: Option, pub protocol: Option, pub ip_list: Vec, pub ip_version: String, @@ -19,6 +20,7 @@ pub struct NftRule { pub description: Option, pub priority: i32, pub enabled: bool, + pub direction: String, pub created_by: Option, pub created_at: DateTime, pub updated_at: DateTime, @@ -28,58 +30,48 @@ pub struct NftRule { pub struct CreateRuleRequest { pub kind: String, pub port: Option, + pub port_end: Option, pub protocol: Option, pub ip_list: Option>, pub ip_version: Option, pub rate_per_min: Option, pub description: Option, pub priority: Option, + pub direction: Option, } -/// Convert individual rule fields into nft rule lines. -/// Used for system-level command generation without constructing a full NftRule. -pub fn rule_line( - kind: &str, - port: Option, - protocol: Option<&str>, - ip_list: &[String], - rate_per_min: Option, -) -> String { - let rule = NftRule { - id: Uuid::nil(), - scope: "global".into(), - agent_id: None, - kind: kind.to_string(), - port: port.map(|p| p as i32), - protocol: protocol.map(|s| s.to_string()), - ip_list: ip_list.to_vec(), - ip_version: "both".into(), - rate_per_min: rate_per_min.map(|r| r as i32), - description: None, - priority: 0, - enabled: true, - created_by: None, - created_at: chrono::Utc::now(), - updated_at: chrono::Utc::now(), - }; - rule_to_nft_lines(&rule).join("\n") -} - -/// Convert a list of NftRules into the body of an nftables chain. -/// Returns lines suitable for insertion inside `chain { ... }`. +/// Convert a list of NftRules into the body of the input chain (lynx-global / lynx-local). +/// Filters to direction = 'input', sorted by priority. pub fn rules_to_nft_chain(rules: &[NftRule]) -> String { - let mut lines: Vec = Vec::new(); - - let mut sorted: Vec<&NftRule> = rules.iter().filter(|r| r.enabled).collect(); + let mut sorted: Vec<&NftRule> = rules + .iter() + .filter(|r| r.enabled && r.direction == "input") + .collect(); sorted.sort_by_key(|r| r.priority); - for rule in sorted { - for line in rule_to_nft_lines(rule) { - lines.push(format!(" {line}")); - } - } + sorted + .iter() + .flat_map(|r| rule_to_nft_lines(r)) + .map(|l| format!(" {l}")) + .collect::>() + .join("\n") +} - lines.join("\n") +/// Convert a list of NftRules into the body of the output chain (lynx-global-output / lynx-local-output). +/// Filters to direction = 'output', sorted by priority. +pub fn rules_to_nft_output_chain(rules: &[NftRule]) -> String { + let mut sorted: Vec<&NftRule> = rules + .iter() + .filter(|r| r.enabled && r.direction == "output") + .collect(); + sorted.sort_by_key(|r| r.priority); + + sorted + .iter() + .flat_map(|r| rule_to_nft_lines(r)) + .map(|l| format!(" {l}")) + .collect::>() + .join("\n") } fn rule_to_nft_lines(rule: &NftRule) -> Vec { @@ -87,6 +79,16 @@ fn rule_to_nft_lines(rule: &NftRule) -> Vec { "allow_port" | "block_port" => port_rule_lines(rule), "allow_ip" | "block_ip" => ip_rule_lines(rule), "rate_limit" => rate_limit_lines(rule), + "drop_invalid_state" => vec!["ct state invalid drop".into()], + "tcp_flag_null" => vec!["tcp flags == 0x0 drop".into()], + "tcp_flag_xmas" => vec!["tcp flags & (fin | psh | urg) == fin | psh | urg drop".into()], + "tcp_flag_ack_new" => vec!["tcp flags & ack == ack ct state new drop".into()], + "icmp_ping_limit" => icmp_ping_limit_lines(rule), + "allow_icmp_errors" => allow_icmp_error_lines(rule), + "allow_ndp" => vec![ + "ip6 nexthdr ipv6-icmp icmpv6 type { nd-router-advert, nd-neighbor-solicit, nd-neighbor-advert } accept".into(), + ], + "block_output_port" => output_port_block_lines(rule), _ => vec![], } } @@ -119,15 +121,25 @@ fn ip_saddr_matches(rule: &NftRule) -> Vec { .collect() } +fn port_spec(rule: &NftRule) -> Option { + let port = rule.port?; + Some(match rule.port_end { + Some(end) => format!("{port}-{end}"), + None => port.to_string(), + }) +} + fn port_rule_lines(rule: &NftRule) -> Vec { - let Some(port) = rule.port else { return vec![] }; + let Some(spec) = port_spec(rule) else { + return vec![]; + }; let proto = rule.protocol.as_deref().unwrap_or("both"); let verd = verdict(&rule.kind); let mut lines = Vec::new(); for proto_kw in protocol_match(proto) { for saddr in ip_saddr_matches(rule) { - lines.push(format!("{saddr}{proto_kw} dport {port} {verd}")); + lines.push(format!("{saddr}{proto_kw} dport {spec} {verd}")); } } lines @@ -145,7 +157,9 @@ fn ip_rule_lines(rule: &NftRule) -> Vec { } fn rate_limit_lines(rule: &NftRule) -> Vec { - let Some(port) = rule.port else { return vec![] }; + let Some(spec) = port_spec(rule) else { + return vec![]; + }; let Some(rate) = rule.rate_per_min else { return vec![]; }; @@ -155,9 +169,63 @@ fn rate_limit_lines(rule: &NftRule) -> Vec { for proto_kw in protocol_match(proto) { for saddr in ip_saddr_matches(rule) { lines.push(format!( - "{saddr}{proto_kw} dport {port} limit rate {rate}/minute accept" + "{saddr}{proto_kw} dport {spec} limit rate {rate}/minute accept" )); } } lines } + +fn icmp_ping_limit_lines(rule: &NftRule) -> Vec { + let rate = rule.rate_per_min.unwrap_or(3); + let mut lines = Vec::new(); + match rule.ip_version.as_str() { + "ipv4" => lines.push(format!( + "ip protocol icmp icmp type echo-request ct state new limit rate {rate}/second burst 10 packets accept" + )), + "ipv6" => lines.push(format!( + "ip6 nexthdr ipv6-icmp icmpv6 type echo-request ct state new limit rate {rate}/second burst 10 packets accept" + )), + _ => { + lines.push(format!( + "ip protocol icmp icmp type echo-request ct state new limit rate {rate}/second burst 10 packets accept" + )); + lines.push(format!( + "ip6 nexthdr ipv6-icmp icmpv6 type echo-request ct state new limit rate {rate}/second burst 10 packets accept" + )); + } + } + lines +} + +fn allow_icmp_error_lines(rule: &NftRule) -> Vec { + let mut lines = Vec::new(); + match rule.ip_version.as_str() { + "ipv4" => lines.push( + "ip protocol icmp icmp type { destination-unreachable, time-exceeded, parameter-problem } accept".into(), + ), + "ipv6" => lines.push( + "ip6 nexthdr ipv6-icmp icmpv6 type { destination-unreachable, packet-too-big, time-exceeded, parameter-problem } accept".into(), + ), + _ => { + lines.push( + "ip protocol icmp icmp type { destination-unreachable, time-exceeded, parameter-problem } accept".into(), + ); + lines.push( + "ip6 nexthdr ipv6-icmp icmpv6 type { destination-unreachable, packet-too-big, time-exceeded, parameter-problem } accept".into(), + ); + } + } + lines +} + +fn output_port_block_lines(rule: &NftRule) -> Vec { + let Some(spec) = port_spec(rule) else { + return vec![]; + }; + let proto = rule.protocol.as_deref().unwrap_or("both"); + protocol_match(proto) + .into_iter() + .map(|p| format!("{p} dport {spec} drop")) + .collect() +} diff --git a/lynx/dashboard/server/src/scheduler.rs b/lynx/dashboard/server/src/scheduler.rs index da51aa7..9ef133d 100644 --- a/lynx/dashboard/server/src/scheduler.rs +++ b/lynx/dashboard/server/src/scheduler.rs @@ -11,12 +11,15 @@ const ROTATION_INTERVAL_DAYS: i64 = 90; /// Top-level scheduler: runs hourly GitHub release check + periodic cert/key rotation. pub async fn run(state: AppState) { + // Run immediately at startup — don't wait an hour for first check. + check_releases(&state).await; + check_rotation(&state).await; + let mut ticker = interval(Duration::from_secs(CHECK_INTERVAL_SECS)); ticker.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip); loop { ticker.tick().await; - check_releases(&state).await; check_rotation(&state).await; } @@ -92,8 +95,8 @@ async fn check_releases(state: &AppState) { async fn dispatch_updates_if_needed(state: &AppState, latest: &str) { let outdated = match sqlx::query!( - "SELECT id, wg_ip::text AS wg_ip, api_port FROM agents \ - WHERE status = 'online' AND (version IS NULL OR version != $1)", + "SELECT id, wg_ip::text AS wg_ip, api_port, COALESCE(arch, 'x86_64') AS arch \ + FROM agents WHERE status = 'online' AND (version IS NULL OR version != $1)", latest ) .fetch_all(&state.db) @@ -125,11 +128,12 @@ async fn dispatch_updates_if_needed(state: &AppState, latest: &str) { let system_user_id = Uuid::nil(); for agent in &outdated { + let arch = agent.arch.as_deref().unwrap_or("x86_64"); let download_url = format!( - "https://github.com/{GITHUB_REPO}/releases/download/agent@{latest}/lynx-agent-linux-x86_64" + "https://github.com/{GITHUB_REPO}/releases/download/agent@{latest}/lynx-agent-linux-{arch}" ); let sig_url = format!( - "https://github.com/{GITHUB_REPO}/releases/download/agent@{latest}/lynx-agent-linux-x86_64.sig" + "https://github.com/{GITHUB_REPO}/releases/download/agent@{latest}/lynx-agent-linux-{arch}.sig" ); let command = serde_json::json!({ "type": "update.self", @@ -182,15 +186,23 @@ async fn dispatch_updates_if_needed(state: &AppState, latest: &str) { } async fn trigger_dashboard_update(state: &AppState, version: &str) { + let arch = match std::env::consts::ARCH { + "aarch64" => "arm64", + a => a, + }; let github_repo = "Jaro-c/Lynx"; let backend_url = format!( - "https://github.com/{github_repo}/releases/download/dashboard@{version}/lynx-dashboard-backend-linux-x86_64" + "https://github.com/{github_repo}/releases/download/dashboard@{version}/lynx-dashboard-backend-linux-{arch}" ); let backend_sig = format!("{backend_url}.sig"); let frontend_url = format!( - "https://github.com/{github_repo}/releases/download/dashboard@{version}/lynx-dashboard-frontend-linux-x86_64" + "https://github.com/{github_repo}/releases/download/dashboard@{version}/lynx-dashboard-frontend-linux-{arch}" ); let frontend_sig = format!("{frontend_url}.sig"); + let frontend_assets_url = format!( + "https://github.com/{github_repo}/releases/download/dashboard@{version}/lynx-dashboard-frontend-assets-linux-{arch}.tar.gz" + ); + let frontend_assets_sig = format!("{frontend_assets_url}.sig"); let log_id = Uuid::now_v7(); let _ = sqlx::query!( @@ -206,23 +218,24 @@ async fn trigger_dashboard_update(state: &AppState, version: &str) { tracing::info!( version, + arch, backend_url = %backend_url, - backend_sig = %backend_sig, frontend_url = %frontend_url, - frontend_sig = %frontend_sig, "scheduler: dashboard self-update initiated" ); - // Actual binary swap happens in crate::update (agent-side pattern). - // For dashboard: download + verify + swap is triggered here, restart handled by Podman. tokio::spawn(crate::update::perform_dashboard_update( - version.to_string(), - backend_url, - backend_sig, - frontend_url, - frontend_sig, - log_id, - state.db.clone(), + crate::update::DashboardUpdateParams { + version: version.to_string(), + backend_url, + backend_sig_url: backend_sig, + frontend_url, + frontend_sig_url: frontend_sig, + frontend_assets_url, + frontend_assets_sig_url: frontend_assets_sig, + log_id, + db: state.db.clone(), + }, )); } diff --git a/lynx/dashboard/server/src/update.rs b/lynx/dashboard/server/src/update.rs index 0ac830d..ec4911d 100644 --- a/lynx/dashboard/server/src/update.rs +++ b/lynx/dashboard/server/src/update.rs @@ -5,23 +5,40 @@ use std::path::PathBuf; use uuid::Uuid; const BINARY_PATH: &str = "/etc/lynx/bin/lynx-dashboard-backend"; +const FRONTEND_BINARY: &str = "/etc/lynx/frontend/lynx-dashboard-frontend"; +const FRONTEND_DIR: &str = "/etc/lynx/frontend"; +const FRONTEND_CONTAINER: &str = "lynx-dashboard-frontend"; +const PODMAN_SOCKET: &str = "/run/podman/podman.sock"; const MAX_DOWNLOAD_BYTES: usize = 200 * 1024 * 1024; -pub async fn perform_dashboard_update( - version: String, - backend_url: String, - backend_sig_url: String, - _frontend_url: String, - _frontend_sig_url: String, - log_id: Uuid, - db: sqlx::PgPool, -) { - let result = do_backend_swap(&version, &backend_url, &backend_sig_url).await; +pub struct DashboardUpdateParams { + pub version: String, + pub backend_url: String, + pub backend_sig_url: String, + pub frontend_url: String, + pub frontend_sig_url: String, + pub frontend_assets_url: String, + pub frontend_assets_sig_url: String, + pub log_id: Uuid, + pub db: sqlx::PgPool, +} + +pub async fn perform_dashboard_update(p: DashboardUpdateParams) { + let result = run_full_update( + &p.version, + &p.backend_url, + &p.backend_sig_url, + &p.frontend_url, + &p.frontend_sig_url, + &p.frontend_assets_url, + &p.frontend_assets_sig_url, + ) + .await; let status = match result { Ok(()) => "success", Err(ref e) => { - tracing::error!(version, "dashboard self-update failed: {e:#}"); + tracing::error!(version = p.version, "dashboard self-update failed: {e:#}"); "failed" } }; @@ -29,41 +46,111 @@ pub async fn perform_dashboard_update( let _ = sqlx::query!( "UPDATE update_log SET status = $1 WHERE id = $2", status, - log_id + p.log_id ) - .execute(&db) + .execute(&p.db) .await; if result.is_ok() { tracing::info!( - version, - "dashboard backend swap complete — exiting for Podman restart" + version = p.version, + "dashboard update complete — exiting for Podman restart" ); std::process::exit(0); } } -async fn do_backend_swap(version: &str, url: &str, sig_url: &str) -> Result<()> { - tracing::info!(version, "downloading dashboard backend binary"); +async fn run_full_update( + version: &str, + backend_url: &str, + backend_sig_url: &str, + frontend_url: &str, + frontend_sig_url: &str, + frontend_assets_url: &str, + frontend_assets_sig_url: &str, +) -> Result<()> { + // Download and verify everything before touching any files. + tracing::info!(version, "downloading and verifying dashboard artifacts"); + let backend_binary = + download_and_verify(backend_url, backend_sig_url, "backend binary").await?; + let frontend_binary = + download_and_verify(frontend_url, frontend_sig_url, "frontend binary").await?; + let frontend_assets = download_and_verify( + frontend_assets_url, + frontend_assets_sig_url, + "frontend assets", + ) + .await?; + + tracing::info!(version, "all signatures verified — applying update"); + + // Update frontend first (backend stays alive to orchestrate). + swap_frontend(&frontend_binary, &frontend_assets).await?; + // Swap backend binary last; process::exit triggers Podman restart with new binary. + swap_backend_binary(&backend_binary)?; + + Ok(()) +} + +async fn download_and_verify(url: &str, sig_url: &str, label: &str) -> Result> { validate_github_url(url)?; validate_github_url(sig_url)?; - - let binary = download_bytes(url).await.context("download binary")?; + let data = download_bytes(url) + .await + .with_context(|| format!("download {label}"))?; let sig = download_bytes(sig_url) .await - .context("download signature")?; + .with_context(|| format!("download {label} signature"))?; + verify_signature(&data, &sig).with_context(|| format!("{label} signature invalid"))?; + tracing::info!(label, bytes = data.len(), "signature verified"); + Ok(data) +} + +async fn swap_frontend(binary: &[u8], assets: &[u8]) -> Result<()> { + // Stop container so the binary file is not in use during swap. + podman_request(&format!("/containers/{FRONTEND_CONTAINER}/stop")) + .await + .context("stop frontend container")?; + + // Swap binary. + let target = PathBuf::from(FRONTEND_BINARY); + let prev = PathBuf::from(format!("{FRONTEND_BINARY}.prev")); + let tmp = PathBuf::from(format!("{FRONTEND_BINARY}.new")); + + std::fs::write(&tmp, binary).context("write frontend binary")?; + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + let mut perms = std::fs::metadata(&tmp)?.permissions(); + perms.set_mode(0o755); + std::fs::set_permissions(&tmp, perms)?; + } + if target.exists() { + std::fs::copy(&target, &prev).context("backup frontend binary to .prev")?; + } + std::fs::rename(&tmp, &target).context("atomic rename frontend binary")?; + + // Extract static assets (overwrites .next/static and public/ in-place). + let assets_owned = assets.to_vec(); + tokio::task::spawn_blocking(move || extract_assets(&assets_owned, FRONTEND_DIR)) + .await + .context("spawn_blocking extract assets")??; - verify_signature(&binary, &sig).context("signature verification failed — update aborted")?; - tracing::info!(version, bytes = binary.len(), "signature verified"); + // Start container with the new binary. + podman_request(&format!("/containers/{FRONTEND_CONTAINER}/start")) + .await + .context("start frontend container")?; + Ok(()) +} + +fn swap_backend_binary(binary: &[u8]) -> Result<()> { let target = PathBuf::from(BINARY_PATH); let prev = PathBuf::from(format!("{BINARY_PATH}.prev")); let tmp = PathBuf::from(format!("{BINARY_PATH}.new")); - // Write new binary to temp path - std::fs::write(&tmp, &binary).context("write new binary to .new")?; - + std::fs::write(&tmp, binary).context("write backend binary")?; #[cfg(unix)] { use std::os::unix::fs::PermissionsExt; @@ -71,14 +158,62 @@ async fn do_backend_swap(version: &str, url: &str, sig_url: &str) -> Result<()> perms.set_mode(0o755); std::fs::set_permissions(&tmp, perms)?; } - - // Back up current binary if target.exists() { - std::fs::copy(&target, &prev).context("backup current binary to .prev")?; + std::fs::copy(&target, &prev).context("backup backend binary to .prev")?; } + std::fs::rename(&tmp, &target).context("atomic rename backend binary")?; - // Atomic swap - std::fs::rename(&tmp, &target).context("atomic rename .new → binary")?; + Ok(()) +} + +fn extract_assets(data: &[u8], dest: &str) -> Result<()> { + use std::io::Write; + use std::process::{Command, Stdio}; + + let mut child = Command::new("tar") + .args(["-xz", "-C", dest]) + .stdin(Stdio::piped()) + .stdout(Stdio::null()) + .stderr(Stdio::piped()) + .spawn() + .context("spawn tar")?; + + if let Some(mut stdin) = child.stdin.take() { + stdin + .write_all(data) + .context("write tarball to tar stdin")?; + } + + let output = child.wait_with_output().context("wait for tar")?; + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + anyhow::bail!("tar extraction failed: {stderr}"); + } + Ok(()) +} + +async fn podman_request(path: &str) -> Result<()> { + use tokio::io::{AsyncReadExt, AsyncWriteExt}; + use tokio::net::UnixStream; + + let mut stream = UnixStream::connect(PODMAN_SOCKET) + .await + .context("connect to Podman socket")?; + + let req = format!("POST {path} HTTP/1.0\r\nHost: localhost\r\nContent-Length: 0\r\n\r\n"); + stream.write_all(req.as_bytes()).await?; + stream.shutdown().await?; + + let mut buf = Vec::with_capacity(1024); + stream.read_to_end(&mut buf).await?; + + let resp = std::str::from_utf8(&buf).unwrap_or(""); + let status_line = resp.lines().next().unwrap_or(""); + + // 2xx = success; 304 = already in target state (container already stopped/started) + if !status_line.contains(" 2") && !status_line.contains(" 304") { + anyhow::bail!("Podman API {path}: {status_line}"); + } Ok(()) } @@ -138,39 +273,31 @@ fn is_blocked_ip(ip: &IpAddr) -> bool { match ip { IpAddr::V4(v4) => { let octets = v4.octets(); - // 10.0.0.0/8 if octets[0] == 10 { return true; } - // 172.16.0.0/12 if octets[0] == 172 && (octets[1] & 0xF0) == 16 { return true; } - // 192.168.0.0/16 if octets[0] == 192 && octets[1] == 168 { return true; } - // 127.0.0.0/8 loopback if octets[0] == 127 { return true; } - // 169.254.0.0/16 link-local if octets[0] == 169 && octets[1] == 254 { return true; } false } IpAddr::V6(v6) => { - // ::1 loopback if v6.is_loopback() { return true; } let segs = v6.segments(); - // fc00::/7 unique local if (segs[0] & 0xFE00) == 0xFC00 { return true; } - // fe80::/10 link-local if (segs[0] & 0xFFC0) == 0xFE80 { return true; } @@ -221,7 +348,7 @@ pub fn spawn_startup_health_guard() { for _ in 0..15 { tokio::time::sleep(std::time::Duration::from_secs(2)).await; if client - .get("http://127.0.0.1:8080/health") + .get("http://127.0.0.1:8080/health") // audit-urls: ok — self health check, not a download .send() .await .map(|r| r.status().is_success()) @@ -270,17 +397,12 @@ fn verify_signature(binary: &[u8], sig_bytes: &[u8]) -> Result<()> { .context("Ed25519 signature invalid") } +const RELEASE_VERIFY_KEY_B64: &str = "OsBV4t+vQSn10FAI8UzAJEBS0IUqp8D2bZtlQYD8j+Q="; + fn load_release_verify_key() -> Result<[u8; 32]> { use base64ct::{Base64, Encoding}; - let raw = if let Ok(path) = std::env::var("RELEASE_VERIFY_KEY_FILE") { - std::fs::read_to_string(&path) - .with_context(|| format!("read RELEASE_VERIFY_KEY_FILE={path}"))? - } else { - std::env::var("RELEASE_VERIFY_KEY") - .or_else(|_| std::env::var("DASHBOARD_VERIFY_KEY")) - .context("RELEASE_VERIFY_KEY not configured")? - }; - let bytes = Base64::decode_vec(raw.trim()).context("base64 decode release verify key")?; + let bytes = Base64::decode_vec(RELEASE_VERIFY_KEY_B64) + .context("decode hardcoded release verify key")?; bytes .try_into() .map_err(|_| anyhow::anyhow!("release verify key must be 32 bytes")) diff --git a/lynx/dashboard/server/tests/helpers.rs b/lynx/dashboard/server/tests/helpers.rs index cf69d0f..8f4ac11 100644 --- a/lynx/dashboard/server/tests/helpers.rs +++ b/lynx/dashboard/server/tests/helpers.rs @@ -43,6 +43,17 @@ pub async fn test_state() -> AppState { let redis = redis::Client::open(redis_url.as_str()).expect("open redis"); let redis_manager = ConnectionManager::new(redis).await.expect("connect redis"); + // Flush testadmin's per-username rate-limit key before each test. + // With --test-threads=1 tests run serially within a binary, so this reset + // fires before every test that calls test_state(), keeping the counter at 0 + // and preventing the 10/15min per-username limit from being exhausted by + // multiple parallel test binaries all logging in as testadmin. + { + use redis::AsyncCommands; + let mut r = redis_manager.clone(); + let _: redis::RedisResult<()> = r.del("rl:login:u:testadmin").await; + } + let sign_seed = [0x42u8; 32]; let signing = SigningKey::from_bytes(&sign_seed); let sign_pub = signing.verifying_key().to_bytes(); diff --git a/lynx/dashboard/setup-dashboard.sh b/lynx/dashboard/setup-dashboard.sh index 82f2999..0890c39 100644 --- a/lynx/dashboard/setup-dashboard.sh +++ b/lynx/dashboard/setup-dashboard.sh @@ -276,24 +276,50 @@ if $existing; then esac fi -# --- Check dependencies ----------------------------------------------------- +# --- Install dependencies --------------------------------------------------- log_section "Checking system dependencies" +_apt_updated=false +_apt_ensure() { + local cmd="$1" pkg="$2" + if command -v "$cmd" &>/dev/null; then + log_ok "$cmd found" + return + fi + log_info "Installing $pkg..." + if ! $_apt_updated; then + # Enable universe repo (needed for podman on Ubuntu) + if command -v add-apt-repository &>/dev/null; then + add-apt-repository -y universe &>/dev/null || true + fi + DEBIAN_FRONTEND=noninteractive apt-get update -qq + _apt_updated=true + fi + DEBIAN_FRONTEND=noninteractive apt-get install -y "$pkg" -qq + if command -v "$cmd" &>/dev/null; then + log_ok "$cmd installed" + else + log_error "Failed to install $pkg (command: $cmd)" + exit 1 + fi +} + _require_cmd() { if ! command -v "$1" &>/dev/null; then - log_error "Required command not found: $1" - log_info "Install it with: $2" + log_error "Required command not found: $1 — $2" exit 1 fi log_ok "$1 found" } -_require_cmd podman "apt install podman" -_require_cmd openssl "apt install openssl" -_require_cmd nft "apt install nftables" -_require_cmd wg "apt install wireguard-tools" -_require_cmd curl "apt install curl" +_apt_ensure podman podman +_apt_ensure openssl openssl +_apt_ensure nft nftables +_apt_ensure wg wireguard-tools +_apt_ensure curl curl +_apt_ensure python3 python3 +_apt_ensure pip3 python3-pip _require_cmd systemctl "systemd required" _require_cmd free "procps required" @@ -425,6 +451,152 @@ printf '%s' "$SETUP_TOKEN" | podman secret create lynx-dashboard-setup-token - log_ok "All secrets generated — values purged from memory" +# --- Download binaries from GitHub Releases --------------------------------- +# +# Binaries are signed with Ed25519. Public key is hardcoded in each binary +# and verified here during install. The private key lives only in GitHub +# Actions secrets — never in the repo. + +log_section "Downloading dashboard binaries" + +GITHUB_REPO="Jaro-c/Lynx" +RELEASE_VERIFY_KEY_B64="OsBV4t+vQSn10FAI8UzAJEBS0IUqp8D2bZtlQYD8j+Q=" + +# Detect architecture +_ARCH=$(uname -m) +case "$_ARCH" in + x86_64) ARCH="x86_64" ;; + aarch64) ARCH="arm64" ;; + *) + log_error "Unsupported architecture: $_ARCH" + exit 1 + ;; +esac +log_info "Architecture: $ARCH" + +# Fetch latest dashboard release tag +log_info "Fetching latest dashboard release..." +LATEST_TAG=$(curl -fsSL \ + "https://api.github.com/repos/${GITHUB_REPO}/releases" \ + | python3 -c " +import sys, json +releases = json.load(sys.stdin) +for r in releases: + tag = r.get('tag_name', '') + if tag.startswith('dashboard@') and not r.get('prerelease'): + print(tag) + break +" 2>/dev/null) + +if [[ -z "$LATEST_TAG" ]]; then + log_error "No dashboard release found in ${GITHUB_REPO}" + exit 1 +fi +log_ok "Latest release: ${LATEST_TAG}" + +RELEASE_BASE="https://github.com/${GITHUB_REPO}/releases/download/${LATEST_TAG}" +BIN_DIR="/etc/lynx/bin" +FRONTEND_DIR="/etc/lynx/frontend" + +mkdir -p "$BIN_DIR" "$FRONTEND_DIR" +chmod 700 "$BIN_DIR" "$FRONTEND_DIR" + +# Verify Ed25519 signature. Args: +_verify_release_sig() { + local file="$1" sig_file="$2" + python3 - "$file" "$sig_file" <<'PYEOF' +import sys, base64 +from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey + +pub_b64 = "OsBV4t+vQSn10FAI8UzAJEBS0IUqp8D2bZtlQYD8j+Q=" +pub_key = Ed25519PublicKey.from_public_bytes(base64.b64decode(pub_b64 + "==")) + +with open(sys.argv[1], "rb") as f: + data = f.read() +with open(sys.argv[2], "rb") as f: + sig = f.read() +try: + pub_key.verify(sig, data) +except Exception as e: + print(f"signature invalid: {e}", file=sys.stderr) + sys.exit(1) +PYEOF +} + +# Ensure cryptography lib is available for signature verification +if ! python3 -c "from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey" 2>/dev/null; then + log_info "Installing Python cryptography library..." + pip3 install --quiet cryptography +fi + +# Download and verify backend binary +log_info "Downloading backend binary..." +BACKEND_FILE="${BIN_DIR}/lynx-dashboard-backend" +BACKEND_TMP="${BIN_DIR}/lynx-dashboard-backend.new" + +curl -fsSL --max-time 300 \ + "${RELEASE_BASE}/lynx-dashboard-backend-linux-${ARCH}" \ + -o "$BACKEND_TMP" +curl -fsSL --max-time 30 \ + "${RELEASE_BASE}/lynx-dashboard-backend-linux-${ARCH}.sig" \ + -o "${BACKEND_TMP}.sig" + +log_info "Verifying backend signature..." +if ! _verify_release_sig "$BACKEND_TMP" "${BACKEND_TMP}.sig"; then + log_error "Backend signature verification FAILED — aborting" + rm -f "$BACKEND_TMP" "${BACKEND_TMP}.sig" + exit 1 +fi +rm -f "${BACKEND_TMP}.sig" +chmod 755 "$BACKEND_TMP" +mv "$BACKEND_TMP" "$BACKEND_FILE" +log_ok "Backend installed: ${BACKEND_FILE}" + +# Download and verify frontend binary + assets +log_info "Downloading frontend binary..." +FRONTEND_BIN_TMP="${FRONTEND_DIR}/lynx-dashboard-frontend.new" +FRONTEND_ASSETS_TMP="${FRONTEND_DIR}/assets.new.tar.gz" + +curl -fsSL --max-time 300 \ + "${RELEASE_BASE}/lynx-dashboard-frontend-linux-${ARCH}" \ + -o "$FRONTEND_BIN_TMP" +curl -fsSL --max-time 30 \ + "${RELEASE_BASE}/lynx-dashboard-frontend-linux-${ARCH}.sig" \ + -o "${FRONTEND_BIN_TMP}.sig" + +log_info "Verifying frontend binary signature..." +if ! _verify_release_sig "$FRONTEND_BIN_TMP" "${FRONTEND_BIN_TMP}.sig"; then + log_error "Frontend binary signature verification FAILED — aborting" + rm -f "$FRONTEND_BIN_TMP" "${FRONTEND_BIN_TMP}.sig" + exit 1 +fi +rm -f "${FRONTEND_BIN_TMP}.sig" +chmod 755 "$FRONTEND_BIN_TMP" + +log_info "Downloading frontend assets..." +curl -fsSL --max-time 300 \ + "${RELEASE_BASE}/lynx-dashboard-frontend-assets-linux-${ARCH}.tar.gz" \ + -o "$FRONTEND_ASSETS_TMP" +curl -fsSL --max-time 30 \ + "${RELEASE_BASE}/lynx-dashboard-frontend-assets-linux-${ARCH}.tar.gz.sig" \ + -o "${FRONTEND_ASSETS_TMP}.sig" + +log_info "Verifying frontend assets signature..." +if ! _verify_release_sig "$FRONTEND_ASSETS_TMP" "${FRONTEND_ASSETS_TMP}.sig"; then + log_error "Frontend assets signature verification FAILED — aborting" + rm -f "$FRONTEND_BIN_TMP" "$FRONTEND_ASSETS_TMP" "${FRONTEND_ASSETS_TMP}.sig" + exit 1 +fi +rm -f "${FRONTEND_ASSETS_TMP}.sig" + +# Place frontend binary and extract assets into FRONTEND_DIR +# Binary runs from FRONTEND_DIR so __dirname resolves static assets correctly +mv "$FRONTEND_BIN_TMP" "${FRONTEND_DIR}/lynx-dashboard-frontend" +tar -xzf "$FRONTEND_ASSETS_TMP" -C "$FRONTEND_DIR" +rm -f "$FRONTEND_ASSETS_TMP" + +log_ok "Frontend installed: ${FRONTEND_DIR}/" + # --- Start services --------------------------------------------------------- log_section "Starting services" diff --git a/lynx/dashboard/ui/bun.lock b/lynx/dashboard/ui/bun.lock index 82b4e06..cd5e27e 100644 --- a/lynx/dashboard/ui/bun.lock +++ b/lynx/dashboard/ui/bun.lock @@ -5,47 +5,47 @@ "": { "name": "test", "dependencies": { - "@base-ui/react": "^1.4.1", + "@base-ui/react": "1.4.1", "@hookform/resolvers": "5.2.2", - "@tanstack/react-table": "^8.21.3", - "class-variance-authority": "^0.7.1", - "clsx": "^2.1.1", - "date-fns": "^4.1.0", - "embla-carousel-react": "^8.6.0", - "input-otp": "^1.4.2", - "lucide-react": "^1.14.0", - "next": "^16.2.6", + "@tanstack/react-table": "8.21.3", + "class-variance-authority": "0.7.1", + "clsx": "2.1.1", + "date-fns": "4.1.0", + "embla-carousel-react": "8.6.0", + "input-otp": "1.4.2", + "lucide-react": "1.16.0", + "next": "16.2.6", "next-intl": "4.12.0", - "next-themes": "^0.4.6", - "radix-ui": "^1.4.3", - "react": "^19.2.6", - "react-day-picker": "^10.0.0", - "react-dom": "^19.2.6", + "next-themes": "0.4.6", + "radix-ui": "1.4.3", + "react": "19.2.6", + "react-day-picker": "10.0.1", + "react-dom": "19.2.6", "react-hook-form": "7.75.0", - "react-resizable-panels": "^4.11.0", - "recharts": "^3.8.1", - "shadcn": "^4.7.0", - "sonner": "^2.0.7", - "tailwind-merge": "^3.6.0", - "tw-animate-css": "^1.4.0", - "vaul": "^1.1.2", + "react-resizable-panels": "4.11.1", + "recharts": "3.8.1", + "shadcn": "4.7.0", + "sonner": "2.0.7", + "tailwind-merge": "3.6.0", + "tw-animate-css": "1.4.0", + "vaul": "1.1.2", "zod": "4.4.3", }, "devDependencies": { - "@biomejs/biome": "^2.4.15", - "@playwright/test": "^1.52.0", - "@tailwindcss/postcss": "^4.3.0", + "@biomejs/biome": "2.4.15", + "@playwright/test": "1.60.0", + "@tailwindcss/postcss": "4.3.0", "@testing-library/jest-dom": "6.9.1", "@testing-library/react": "16.3.2", "@testing-library/user-event": "14.6.1", - "@types/node": "^24.12.4", - "@types/react": "^19.2.14", - "@types/react-dom": "^19.2.3", + "@types/node": "24.12.4", + "@types/react": "19.2.14", + "@types/react-dom": "19.2.3", "@vitejs/plugin-react": "6.0.2", - "babel-plugin-react-compiler": "^1.0.0", + "babel-plugin-react-compiler": "1.0.0", "jsdom": "29.1.1", - "tailwindcss": "^4.3.0", - "typescript": "^6.0.3", + "tailwindcss": "4.3.0", + "typescript": "6.0.3", "vitest": "4.1.6", }, }, @@ -1048,7 +1048,7 @@ "lru-cache": ["lru-cache@11.3.6", "", {}, "sha512-Gf/KoL3C/MlI7Bt0PGI9I+TeTC/I6r/csU58N4BSNc4lppLBeKsOdFYkK+dX0ABDUMJNfCHTyPpzwwO21Awd3A=="], - "lucide-react": ["lucide-react@1.14.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-+1mdWcfSJVUsaTIjN9zoezmUhfXo5l0vP7ekBMPo3jcS/aIkxHnXqAPsByszMZx/Y8oQBRJxJx5xg+RH3urzxA=="], + "lucide-react": ["lucide-react@1.16.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-dYwyPzb4MEKpGUmNYk3WKWPnMrHs3FKM+q94kAnJrcDIqqn1hq2xY8scaS2ovsOCM5D51ey2gaRG3PBb1vgoYQ=="], "lz-string": ["lz-string@1.5.0", "", { "bin": { "lz-string": "bin/bin.js" } }, "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ=="], @@ -1188,7 +1188,7 @@ "react": ["react@19.2.6", "", {}, "sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q=="], - "react-day-picker": ["react-day-picker@10.0.0", "", { "dependencies": { "@date-fns/tz": "^1.4.1", "date-fns": "^4.1.0" }, "peerDependencies": { "react": ">=16.8.0" } }, "sha512-lrEXo5wFPsq5LTcayelM3BPueD00v7zbdipAY+EIdPcseVykYwkOWx4Ujn/EtbBvpnp8ZPUHol17HXH6kVbZoA=="], + "react-day-picker": ["react-day-picker@10.0.1", "", { "dependencies": { "@date-fns/tz": "^1.4.1", "date-fns": "^4.1.0" }, "peerDependencies": { "@types/react": ">=16.8.0", "react": ">=16.8.0" }, "optionalPeers": ["@types/react"] }, "sha512-eNh6BlwcYInWaJtRv18mXQ06Ys/H6rdTZAnTaSdOYJuTpwP1JMCHNd1FDRadA+gbeinq+psdULN5Xnowy9mV8w=="], "react-dom": ["react-dom@19.2.6", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.6" } }, "sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g=="], @@ -1202,7 +1202,7 @@ "react-remove-scroll-bar": ["react-remove-scroll-bar@2.3.8", "", { "dependencies": { "react-style-singleton": "^2.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q=="], - "react-resizable-panels": ["react-resizable-panels@4.11.0", "", { "peerDependencies": { "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" } }, "sha512-LPk/AkFDGkg7SsbOyL93ojrE6E7lhrxxDwnYNjfmnSeI6BE7Sje6dB24PXgZk8DeugdeXNk1LO+ohRqIjhxiLw=="], + "react-resizable-panels": ["react-resizable-panels@4.11.1", "", { "peerDependencies": { "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" } }, "sha512-kA4w58V6wYdRLm2rg9pzroZwGlqBLul1FjMP0J8kqTo3zSHtjeH+LXmZaldCo6+HWqs1e5hOcPoajKXdOze37Q=="], "react-style-singleton": ["react-style-singleton@2.2.3", "", { "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ=="], diff --git a/lynx/dashboard/ui/components.json b/lynx/dashboard/ui/components.json index 2cfca44..91656b2 100644 --- a/lynx/dashboard/ui/components.json +++ b/lynx/dashboard/ui/components.json @@ -1,25 +1,25 @@ { "$schema": "https://ui.shadcn.com/schema.json", - "style": "radix-nova", + "aliases": { + "components": "@/components", + "hooks": "@/hooks", + "lib": "@/lib", + "ui": "@/components/ui", + "utils": "@/lib/utils" + }, + "iconLibrary": "lucide", + "menuAccent": "subtle", + "menuColor": "default", + "registries": {}, "rsc": true, - "tsx": true, + "rtl": false, + "style": "radix-nova", "tailwind": { + "baseColor": "neutral", "config": "", "css": "src/app/globals.css", - "baseColor": "neutral", "cssVariables": true, "prefix": "" }, - "iconLibrary": "lucide", - "rtl": false, - "aliases": { - "components": "@/components", - "utils": "@/lib/utils", - "ui": "@/components/ui", - "lib": "@/lib", - "hooks": "@/hooks" - }, - "menuColor": "default", - "menuAccent": "subtle", - "registries": {} + "tsx": true } diff --git a/lynx/dashboard/ui/messages/en.json b/lynx/dashboard/ui/messages/en.json index 14bd6e6..e3f9358 100644 --- a/lynx/dashboard/ui/messages/en.json +++ b/lynx/dashboard/ui/messages/en.json @@ -1,417 +1,417 @@ { - "auth": { - "login": { - "title": "Sign in to {company}", - "subtitle": "Distributed infrastructure orchestration", - "username": "Username", - "password": "Password", - "submit": "Sign in", - "submitting": "Signing in...", - "noAccount": "Don't have an account?", - "register": "Create account", - "invalidCredentials": "Invalid username or password", - "rateLimited": "Too many attempts. Try again in {minutes} minutes.", - "serverError": "Something went wrong. Please try again." - }, - "register": { - "title": "Create your account", - "subtitle": "Start orchestrating your infrastructure", - "username": "Username", - "email": "Email", - "password": "Password", - "submit": "Create account", - "submitting": "Creating account...", - "hasAccount": "Already have an account?", - "login": "Sign in", - "usernameTaken": "Username already taken", - "emailTaken": "Email already registered", - "serverError": "Something went wrong. Please try again.", - "validation": { - "usernameMin": "Username must be at least 3 characters", - "usernameMax": "Username cannot exceed 32 characters", - "usernameChars": "Only lowercase letters, numbers, - and _ allowed", - "usernameEdge": "Cannot start or end with - or _", - "usernameReserved": "This username is reserved", - "emailInvalid": "Enter a valid email address", - "passwordMin": "Password must be at least 12 characters", - "passwordMax": "Password cannot exceed 30 characters", - "passwordUppercase": "Must contain at least one uppercase letter", - "passwordLowercase": "Must contain at least one lowercase letter", - "passwordNumber": "Must contain at least one number", - "passwordSpecial": "Must contain at least one special character" - } - } - }, - "app": { - "nav": { - "overview": "Overview", - "agents": "Agents", - "organizations": "Organizations", - "settings": "Settings", - "admin": "Admin", - "signOut": "Sign out" - }, - "organizations": { - "title": "Organizations", - "create": "New Organization", - "slugConflict": "Slug already taken. Choose a different name.", - "createError": "Failed to create organization.", - "noOrgs": "No organizations yet", - "noOrgsDesc": "Create an organization to start managing projects and containers.", - "slug": "Slug:", - "members": "Members:", - "invite": "Invite member", - "inviteTitle": "Invite a member", - "inviteUsername": "Username", - "inviteRole": "Role", - "inviteSubmit": "Send invite", - "inviteSuccess": "Member invited.", - "inviteError": "Failed to invite member.", - "removeMember": "Remove", - "removeMemberSuccess": "Member removed.", - "removeMemberError": "Failed to remove member.", - "noMembers": "No members yet.", - "orgId": "ID:", - "projects": "Projects", - "noProjects": "No projects yet.", - "createProject": "New project", - "createProjectTitle": "Create project", - "projectName": "Name", - "projectSlug": "Slug", - "projectAgent": "Target agent", - "projectNoAgents": "No agents available. Register an agent first.", - "projectCreate": "Create project", - "projectCreateSuccess": "Project created.", - "projectSlugConflict": "Slug already taken in this organization.", - "projectCreateError": "Failed to create project." - }, - "projects": { - "orgs": "Organizations", - "org": "Organization", - "slug": "Slug:", - "verticalScale": "Vertical Scaling", - "verticalScaleDesc": "Update CPU and memory limits for a container in this project. The container will be updated live without restart when possible.", - "containerName": "Container name", - "cpus": "CPUs", - "memoryMb": "Memory (MB)", - "apply": "Apply limits", - "applySuccess": "Container limits updated.", - "applyError": "Failed to update container limits.", - "projectId": "Project ID:", - "containers": "Containers", - "noContainers": "No containers running.", - "cStart": "Start", - "cStop": "Stop", - "cRestart": "Restart", - "cRemove": "Remove", - "cActionSuccess": "done", - "cActionError": "Container action failed.", - "deploy": "Deploy container", - "deployBtn": "Deploy", - "deploySuccess": "Container deployed.", - "deployError": "Deploy failed.", - "cName": "Container name", - "cImage": "Image", - "cPorts": "Ports (comma-separated)", - "cEnv": "Env vars (comma-separated)", - "horizontalScale": "Horizontal Scaling", - "horizontalScaleDesc": "Deploy replicas on another agent over a direct WireGuard data-plane tunnel. Traffic stays agent-to-agent — never routed through the dashboard.", - "addTunnel": "Add tunnel", - "addTunnelTitle": "Scale to another agent", - "tunnelTargetAgent": "Target agent", - "tunnelImage": "Container image", - "tunnelReplicas": "Replica count", - "tunnelConfirm": "Establish tunnel & deploy", - "tunnelSuccess": "Tunnel established and replicas deployed.", - "tunnelError": "Failed to establish tunnel.", - "tunnelTeardownSuccess": "Tunnel torn down.", - "tunnelTeardownError": "Failed to tear down tunnel.", - "noTunnels": "No active tunnels.", - "tunnelReplicaCount": "Replicas" - }, - "settings": { - "title": "Settings", - "profile": "Profile", - "profileUsername": "Username", - "changePassword": "Change password", - "currentPassword": "Current password", - "newPassword": "New password", - "changePasswordBtn": "Change password", - "changePasswordSuccess": "Password changed. You have been signed out.", - "changePasswordWrong": "Current password is incorrect.", - "changePasswordError": "Failed to change password.", - "singleSession": "Single active session", - "singleSessionDesc": "When enabled, signing in on a new device will invalidate all other active sessions.", - "singleSessionSuccess": "Single session setting updated.", - "singleSessionError": "Failed to update setting.", - "domain": "Domain", - "domainDesc": "Configure a custom domain with Let's Encrypt TLS. Requires DNS A record pointing to this server before setup.", - "domainCurrent": "Current domain:", - "domainNone": "Using IP address (no custom domain)", - "domainInput": "Domain (e.g. panel.example.com)", - "domainEmail": "Email for Let's Encrypt", - "domainSetup": "Configure domain", - "domainPending": "Setup in progress…", - "domainActive": "Active", - "domainError": "Error", - "domainUnconfigured": "Not configured", - "domainVerify": "Verify DNS", - "domainDnsOk": "DNS resolved correctly.", - "domainDnsFail": "Domain does not resolve. Check your DNS A record.", - "domainVerifyError": "Failed to verify.", - "domainSetupError": "Failed to start domain setup.", - "domainHsts": "HSTS", - "domainHstsDesc": "Force HTTPS for all visitors. Only enable after domain and TLS cert are confirmed working.", - "domainHstsEnable": "Enable HSTS", - "domainHstsDisable": "Disable HSTS", - "domainHstsSuccess": "HSTS updated.", - "domainHstsError": "Failed to update HSTS.", - "domainClosePort": "Close port 19443", - "domainClosePortDesc": "Remove direct IP access on port 19443. Only possible after domain is active. Requires SSH to re-enable.", - "domainClosePortBtn": "Close port 19443", - "domainClosePortConfirm": "This will remove access on port 19443. You will only be able to reach the panel via your configured domain. This cannot be undone remotely. Continue?", - "domainClosePortSuccess": "Port 19443 closed.", - "domainClosePortError": "Failed to close port.", - "domainCert": "Certificate:", - "domainCertSelfSigned": "Self-signed", - "domainCertLE": "Let's Encrypt", - "domainCertCloudflare": "Cloudflare Origin", - "domainCertCustom": "Custom", - "domainCertExpires": "Expires:", - "domainCertUpload": "Upload Certificate", - "domainCertUploadCloudflare": "Cloudflare Origin", - "domainCertUploadCustom": "Custom Certificate", - "domainCertPem": "Certificate (PEM)", - "domainCertPemPlaceholder": "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----", - "domainCertKeyPem": "Private Key (PEM)", - "domainCertKeyPemPlaceholder": "-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----", - "domainCertKeyOptional": "Private key is not required for Cloudflare Origin certificates.", - "domainCertUploadSuccess": "Certificate uploaded and applied.", - "domainCertUploadError": "Failed to upload certificate.", - "security": "Security", - "rotationLog": "Key rotation history", - "rotationLogEmpty": "No rotations recorded.", - "rotationLogReason": "Reason", - "rotationLogScope": "Scope", - "rotateKeys": "Rotate JWT Keys", - "rotateKeysDesc": "Invalidates all active sessions. Everyone must sign in again.", - "rotateKeysBtn": "Rotate & sign out", - "rotateKeysConfirm": "This will invalidate all active sessions, including yours. Continue?", - "sessions": "Active Sessions", - "noSessions": "No active sessions found.", - "lastUsed": "Last used:", - "revoke": "Revoke", - "revokeSuccess": "Session revoked.", - "revokeError": "Failed to revoke session.", - "updates": "Updates", - "updatesDesc": "Check GitHub Releases for new versions and push updates to all connected agents.", - "updateCheck": "Check for updates", - "updateCurrent": "Current:", - "updateLatest": "Latest:", - "updateUpToDate": "Up to date", - "updateAvailable": "Update available", - "updateTrigger": "Trigger update on all agents", - "updateTriggerSuccess": "Update triggered. Agents will update shortly.", - "updateTriggerError": "Failed to trigger update.", - "updateCheckError": "Failed to check for updates.", - "migration": "Dashboard Migration", - "migrationDesc": "Move this dashboard to another VPS. Requires a fresh dashboard installation on the target VPS. Agent SSH access may be needed if any agent fails to reconnect automatically.", - "migrationSourceTitle": "Migrate to another VPS (this is VPS-A)", - "migrationSourceDesc": "Enter the target VPS URL and the migration token from VPS-B to start migration.", - "migrationTargetUrl": "Target VPS URL", - "migrationToken": "Migration token (from VPS-B)", - "migrationStart": "Start migration", - "migrationTargetTitle": "Receive migration (this is VPS-B)", - "migrationTargetDesc": "Generate a migration token to give to VPS-A.", - "migrationPrepare": "Generate migration token", - "migrationPreparedToken": "Copy this token to VPS-A:", - "migrationCopyToken": "Copy", - "migrationAbort": "Abort migration", - "migrationConfirmShutdown": "Confirm shutdown", - "migrationConfirmShutdownMsg": "All agents have confirmed. This will shut down the current dashboard. Make sure VPS-B is fully operational. Continue?", - "migrationStatusIdle": "Idle", - "migrationStatusPreparing": "Preparing", - "migrationStatusTransferring": "Transferring", - "migrationStatusNotifying": "Notifying agents", - "migrationStatusWaiting": "Waiting for agents", - "migrationStatusCompleted": "Completed", - "migrationStatusAborted": "Aborted", - "migrationStatusError": "Error", - "migrationAgentsProgress": "{confirmed} of {total} agents confirmed", - "migrationPrepareError": "Failed to prepare migration.", - "migrationStartError": "Failed to start migration.", - "migrationAbortSuccess": "Migration aborted.", - "migrationAbortError": "Failed to abort migration.", - "migrationShutdownError": "Failed to confirm shutdown.", - "branding": "White-label Branding", - "brandingCompanyName": "Company Name", - "brandingLogoUrl": "Logo URL", - "brandingPrimaryColor": "Primary", - "brandingSecondaryColor": "Secondary", - "brandingAccentColor": "Accent", - "brandingSave": "Save branding", - "brandingSaved": "Branding updated.", - "brandingError": "Failed to update branding." - }, - "auditLog": { - "title": "Audit Log", - "noEntries": "No audit log entries yet.", - "command": "Command", - "result": "Result", - "user": "User", - "org": "Org", - "time": "Time", - "hash": "Hash", - "error": "Error", - "loadMore": "Load more", - "back": "Back to agents", - "resultSuccess": "success", - "resultRejected": "rejected", - "resultFailed": "failed" - }, - "agents": { - "title": "Agents", - "register": "Register Agent", - "registerSuccess": "Agent registered", - "registerSuccessDesc": "Copy the credentials below. The sync token is shown only once.", - "agentId": "Agent ID", - "wgIp": "WireGuard IP:", - "syncToken": "Sync Token", - "warnOnce": "The sync token will never be shown again. Copy it now.", - "done": "Done", - "noAgents": "No agents connected", - "noAgentsDesc": "Run the agent install script on a VPS to connect it here.", - "nftDiverged": "nftables ruleset modified outside Lynx", - "nftRestore": "Restore Lynx rules", - "nftAccept": "Accept changes", - "nftRestoreSuccess": "nftables ruleset restored.", - "nftAcceptSuccess": "Manual changes accepted.", - "nftError": "Failed to resolve nftables divergence.", - "auditLog": "Audit log", - "version": "Version:", - "lastHeartbeat": "Last heartbeat:", - "wgIpLabel": "WireGuard IP:", - "name": "Name:", - "notFound": "Agent not found.", - "backToAgents": "← Agents", - "detailTitle": "Agent Detail", - "reboot": "Reboot VPS", - "rebootConfirm": "This will reboot the VPS. The agent will reconnect automatically after boot. Continue?", - "rebootSuccess": "Reboot command sent.", - "rebootError": "Failed to send reboot command.", - "deleteAgent": "Remove agent", - "deleteConfirm": "Remove this agent from the dashboard? The agent will lose its WireGuard connection. Continue?", - "deleteSuccess": "Agent removed.", - "deleteError": "Failed to remove agent.", - "nftRules": "Firewall Rules", - "nftRulesDesc": "nftables rules applied to this agent. Changes pushed automatically.", - "nftGlobal": "Global rules", - "nftLocal": "Local rules (this agent)", - "nftStatus": "Status", - "nftChecksumOk": "In sync", - "nftChecksumMismatch": "Diverged", - "nftAddRule": "Add rule", - "nftKind": "Type", - "nftPort": "Port", - "nftProtocol": "Protocol", - "nftIpList": "IP list (CIDRs, comma-separated)", - "nftRatePerMin": "Connections per minute", - "nftDescription": "Description (optional)", - "nftPriority": "Priority", - "nftCreate": "Create rule", - "nftCreateSuccess": "Rule created.", - "nftCreateError": "Failed to create rule.", - "nftDeleteSuccess": "Rule deleted.", - "nftDeleteError": "Failed to delete rule.", - "nftPush": "Push to agent", - "nftPushSuccess": "Rules pushed.", - "nftPushError": "Failed to push rules.", - "nftNoRules": "No rules defined.", - "nftKindAllowPort": "Allow port", - "nftKindBlockPort": "Block port", - "nftKindAllowIp": "Allow IP", - "nftKindBlockIp": "Block IP", - "nftKindRateLimit": "Rate limit", - "nftProtoTcp": "TCP", - "nftProtoUdp": "UDP", - "nftProtoBoth": "TCP + UDP", - "metricsLive": "Live metrics", - "metricsCpu": "CPU", - "metricsMemory": "Memory", - "metricsDisk": "Disk", - "metricsConnecting": "Connecting to agent...", - "metricsAgentOffline": "Agent offline", - "metricsOffline": "Metrics unavailable", - "status": { - "online": "Online", - "lockdown": "Lockdown", - "offline": "Offline" - } - }, - "notifications": { - "label": "Notifications", - "title": "Notifications", - "clearAll": "Clear all", - "empty": "No alerts", - "event": { - "heartbeat_lost": "Heartbeat lost", - "lockdown": "Agent in lockdown", - "nftables_divergence": "nftables divergence", - "conflicting_software_detected": "Conflicting software" - } - }, - "overview": { - "title": "Overview", - "agentsOnline": "Agents Online", - "organizations": "Organizations", - "recentEvents": "Recent Events", - "noEvents": "No recent events.", - "noAgents": "No agents connected", - "noAgentsDesc": "Connect a VPS agent to start managing your infrastructure.", - "securityAlerts": "Security Alerts", - "noAlerts": "No active alerts.", - "acknowledge": "Dismiss", - "acknowledged": "Alert dismissed.", - "acknowledgeError": "Failed to dismiss alert." - } - }, - "branding": { - "madeWith": "Made with love by", - "author": "Jaroc" - }, - "admin": { - "title": "Admin", - "users": "Users", - "roles": "Roles", - "deleteUser": "Delete user", - "deleteConfirm": "Delete user", - "deleteSuccess": "User deleted.", - "deleteError": "Failed to delete user.", - "forcePasswordChange": "Force password reset", - "forcePasswordChangeSuccess": "Password reset forced.", - "forcePasswordChangeError": "Failed to force password reset.", - "addRole": "Add role", - "addRoleSuccess": "Role assigned.", - "addRoleError": "Failed to assign role.", - "removeRole": "Remove role", - "removeRoleSuccess": "Role removed.", - "removeRoleError": "Failed to remove role.", - "noRoles": "No roles assigned.", - "selectRole": "Select role", - "createRole": "New role", - "createRoleSuccess": "Role created.", - "createRoleError": "Failed to create role.", - "deleteRole": "Delete role", - "deleteRoleConfirm": "Delete role", - "deleteRoleSuccess": "Role deleted.", - "deleteRoleError": "Failed to delete role.", - "addPermission": "Add permission", - "addPermissionSuccess": "Permission added.", - "addPermissionError": "Failed to add permission.", - "removePermission": "Remove permission", - "removePermissionSuccess": "Permission removed.", - "removePermissionError": "Failed to remove permission.", - "roleName": "Role name", - "noPermissions": "No permissions assigned." - } + "admin": { + "addPermission": "Add permission", + "addPermissionError": "Failed to add permission.", + "addPermissionSuccess": "Permission added.", + "addRole": "Add role", + "addRoleError": "Failed to assign role.", + "addRoleSuccess": "Role assigned.", + "createRole": "New role", + "createRoleError": "Failed to create role.", + "createRoleSuccess": "Role created.", + "deleteConfirm": "Delete user", + "deleteError": "Failed to delete user.", + "deleteRole": "Delete role", + "deleteRoleConfirm": "Delete role", + "deleteRoleError": "Failed to delete role.", + "deleteRoleSuccess": "Role deleted.", + "deleteSuccess": "User deleted.", + "deleteUser": "Delete user", + "forcePasswordChange": "Force password reset", + "forcePasswordChangeError": "Failed to force password reset.", + "forcePasswordChangeSuccess": "Password reset forced.", + "noPermissions": "No permissions assigned.", + "noRoles": "No roles assigned.", + "removePermission": "Remove permission", + "removePermissionError": "Failed to remove permission.", + "removePermissionSuccess": "Permission removed.", + "removeRole": "Remove role", + "removeRoleError": "Failed to remove role.", + "removeRoleSuccess": "Role removed.", + "roleName": "Role name", + "roles": "Roles", + "selectRole": "Select role", + "title": "Admin", + "users": "Users" + }, + "app": { + "agents": { + "agentId": "Agent ID", + "auditLog": "Audit log", + "backToAgents": "← Agents", + "deleteAgent": "Remove agent", + "deleteConfirm": "Remove this agent from the dashboard? The agent will lose its WireGuard connection. Continue?", + "deleteError": "Failed to remove agent.", + "deleteSuccess": "Agent removed.", + "detailTitle": "Agent Detail", + "done": "Done", + "lastHeartbeat": "Last heartbeat:", + "metricsAgentOffline": "Agent offline", + "metricsConnecting": "Connecting to agent...", + "metricsCpu": "CPU", + "metricsDisk": "Disk", + "metricsLive": "Live metrics", + "metricsMemory": "Memory", + "metricsOffline": "Metrics unavailable", + "name": "Name:", + "nftAccept": "Accept changes", + "nftAcceptSuccess": "Manual changes accepted.", + "nftAddRule": "Add rule", + "nftChecksumMismatch": "Diverged", + "nftChecksumOk": "In sync", + "nftCreate": "Create rule", + "nftCreateError": "Failed to create rule.", + "nftCreateSuccess": "Rule created.", + "nftDeleteError": "Failed to delete rule.", + "nftDeleteSuccess": "Rule deleted.", + "nftDescription": "Description (optional)", + "nftDiverged": "nftables ruleset modified outside Lynx", + "nftError": "Failed to resolve nftables divergence.", + "nftGlobal": "Global rules", + "nftIpList": "IP list (CIDRs, comma-separated)", + "nftKind": "Type", + "nftKindAllowIp": "Allow IP", + "nftKindAllowPort": "Allow port", + "nftKindBlockIp": "Block IP", + "nftKindBlockPort": "Block port", + "nftKindRateLimit": "Rate limit", + "nftLocal": "Local rules (this agent)", + "nftNoRules": "No rules defined.", + "nftPort": "Port", + "nftPriority": "Priority", + "nftProtoBoth": "TCP + UDP", + "nftProtocol": "Protocol", + "nftProtoTcp": "TCP", + "nftProtoUdp": "UDP", + "nftPush": "Push to agent", + "nftPushError": "Failed to push rules.", + "nftPushSuccess": "Rules pushed.", + "nftRatePerMin": "Connections per minute", + "nftRestore": "Restore Lynx rules", + "nftRestoreSuccess": "nftables ruleset restored.", + "nftRules": "Firewall Rules", + "nftRulesDesc": "nftables rules applied to this agent. Changes pushed automatically.", + "nftStatus": "Status", + "noAgents": "No agents connected", + "noAgentsDesc": "Run the agent install script on a VPS to connect it here.", + "notFound": "Agent not found.", + "reboot": "Reboot VPS", + "rebootConfirm": "This will reboot the VPS. The agent will reconnect automatically after boot. Continue?", + "rebootError": "Failed to send reboot command.", + "rebootSuccess": "Reboot command sent.", + "register": "Register Agent", + "registerSuccess": "Agent registered", + "registerSuccessDesc": "Copy the credentials below. The sync token is shown only once.", + "status": { + "lockdown": "Lockdown", + "offline": "Offline", + "online": "Online" + }, + "syncToken": "Sync Token", + "title": "Agents", + "version": "Version:", + "warnOnce": "The sync token will never be shown again. Copy it now.", + "wgIp": "WireGuard IP:", + "wgIpLabel": "WireGuard IP:" + }, + "auditLog": { + "back": "Back to agents", + "command": "Command", + "error": "Error", + "hash": "Hash", + "loadMore": "Load more", + "noEntries": "No audit log entries yet.", + "org": "Org", + "result": "Result", + "resultFailed": "failed", + "resultRejected": "rejected", + "resultSuccess": "success", + "time": "Time", + "title": "Audit Log", + "user": "User" + }, + "nav": { + "admin": "Admin", + "agents": "Agents", + "organizations": "Organizations", + "overview": "Overview", + "settings": "Settings", + "signOut": "Sign out" + }, + "notifications": { + "clearAll": "Clear all", + "empty": "No alerts", + "event": { + "conflicting_software_detected": "Conflicting software", + "heartbeat_lost": "Heartbeat lost", + "lockdown": "Agent in lockdown", + "nftables_divergence": "nftables divergence" + }, + "label": "Notifications", + "title": "Notifications" + }, + "organizations": { + "create": "New Organization", + "createError": "Failed to create organization.", + "createProject": "New project", + "createProjectTitle": "Create project", + "invite": "Invite member", + "inviteError": "Failed to invite member.", + "inviteRole": "Role", + "inviteSubmit": "Send invite", + "inviteSuccess": "Member invited.", + "inviteTitle": "Invite a member", + "inviteUsername": "Username", + "members": "Members:", + "noMembers": "No members yet.", + "noOrgs": "No organizations yet", + "noOrgsDesc": "Create an organization to start managing projects and containers.", + "noProjects": "No projects yet.", + "orgId": "ID:", + "projectAgent": "Target agent", + "projectCreate": "Create project", + "projectCreateError": "Failed to create project.", + "projectCreateSuccess": "Project created.", + "projectName": "Name", + "projectNoAgents": "No agents available. Register an agent first.", + "projectSlug": "Slug", + "projectSlugConflict": "Slug already taken in this organization.", + "projects": "Projects", + "removeMember": "Remove", + "removeMemberError": "Failed to remove member.", + "removeMemberSuccess": "Member removed.", + "slug": "Slug:", + "slugConflict": "Slug already taken. Choose a different name.", + "title": "Organizations" + }, + "overview": { + "acknowledge": "Dismiss", + "acknowledged": "Alert dismissed.", + "acknowledgeError": "Failed to dismiss alert.", + "agentsOnline": "Agents Online", + "noAgents": "No agents connected", + "noAgentsDesc": "Connect a VPS agent to start managing your infrastructure.", + "noAlerts": "No active alerts.", + "noEvents": "No recent events.", + "organizations": "Organizations", + "recentEvents": "Recent Events", + "securityAlerts": "Security Alerts", + "title": "Overview" + }, + "projects": { + "addTunnel": "Add tunnel", + "addTunnelTitle": "Scale to another agent", + "apply": "Apply limits", + "applyError": "Failed to update container limits.", + "applySuccess": "Container limits updated.", + "cActionError": "Container action failed.", + "cActionSuccess": "done", + "cEnv": "Env vars (comma-separated)", + "cImage": "Image", + "cName": "Container name", + "containerName": "Container name", + "containers": "Containers", + "cPorts": "Ports (comma-separated)", + "cpus": "CPUs", + "cRemove": "Remove", + "cRestart": "Restart", + "cStart": "Start", + "cStop": "Stop", + "deploy": "Deploy container", + "deployBtn": "Deploy", + "deployError": "Deploy failed.", + "deploySuccess": "Container deployed.", + "horizontalScale": "Horizontal Scaling", + "horizontalScaleDesc": "Deploy replicas on another agent over a direct WireGuard data-plane tunnel. Traffic stays agent-to-agent — never routed through the dashboard.", + "memoryMb": "Memory (MB)", + "noContainers": "No containers running.", + "noTunnels": "No active tunnels.", + "org": "Organization", + "orgs": "Organizations", + "projectId": "Project ID:", + "slug": "Slug:", + "tunnelConfirm": "Establish tunnel & deploy", + "tunnelError": "Failed to establish tunnel.", + "tunnelImage": "Container image", + "tunnelReplicaCount": "Replicas", + "tunnelReplicas": "Replica count", + "tunnelSuccess": "Tunnel established and replicas deployed.", + "tunnelTargetAgent": "Target agent", + "tunnelTeardownError": "Failed to tear down tunnel.", + "tunnelTeardownSuccess": "Tunnel torn down.", + "verticalScale": "Vertical Scaling", + "verticalScaleDesc": "Update CPU and memory limits for a container in this project. The container will be updated live without restart when possible." + }, + "settings": { + "branding": "White-label Branding", + "brandingAccentColor": "Accent", + "brandingCompanyName": "Company Name", + "brandingError": "Failed to update branding.", + "brandingLogoUrl": "Logo URL", + "brandingPrimaryColor": "Primary", + "brandingSave": "Save branding", + "brandingSaved": "Branding updated.", + "brandingSecondaryColor": "Secondary", + "changePassword": "Change password", + "changePasswordBtn": "Change password", + "changePasswordError": "Failed to change password.", + "changePasswordSuccess": "Password changed. You have been signed out.", + "changePasswordWrong": "Current password is incorrect.", + "currentPassword": "Current password", + "domain": "Domain", + "domainActive": "Active", + "domainCert": "Certificate:", + "domainCertCloudflare": "Cloudflare Origin", + "domainCertCustom": "Custom", + "domainCertExpires": "Expires:", + "domainCertKeyOptional": "Private key is not required for Cloudflare Origin certificates.", + "domainCertKeyPem": "Private Key (PEM)", + "domainCertKeyPemPlaceholder": "-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----", + "domainCertLE": "Let's Encrypt", + "domainCertPem": "Certificate (PEM)", + "domainCertPemPlaceholder": "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----", + "domainCertSelfSigned": "Self-signed", + "domainCertUpload": "Upload Certificate", + "domainCertUploadCloudflare": "Cloudflare Origin", + "domainCertUploadCustom": "Custom Certificate", + "domainCertUploadError": "Failed to upload certificate.", + "domainCertUploadSuccess": "Certificate uploaded and applied.", + "domainClosePort": "Close port 19443", + "domainClosePortBtn": "Close port 19443", + "domainClosePortConfirm": "This will remove access on port 19443. You will only be able to reach the panel via your configured domain. This cannot be undone remotely. Continue?", + "domainClosePortDesc": "Remove direct IP access on port 19443. Only possible after domain is active. Requires SSH to re-enable.", + "domainClosePortError": "Failed to close port.", + "domainClosePortSuccess": "Port 19443 closed.", + "domainCurrent": "Current domain:", + "domainDesc": "Configure a custom domain with Let's Encrypt TLS. Requires DNS A record pointing to this server before setup.", + "domainDnsFail": "Domain does not resolve. Check your DNS A record.", + "domainDnsOk": "DNS resolved correctly.", + "domainEmail": "Email for Let's Encrypt", + "domainError": "Error", + "domainHsts": "HSTS", + "domainHstsDesc": "Force HTTPS for all visitors. Only enable after domain and TLS cert are confirmed working.", + "domainHstsDisable": "Disable HSTS", + "domainHstsEnable": "Enable HSTS", + "domainHstsError": "Failed to update HSTS.", + "domainHstsSuccess": "HSTS updated.", + "domainInput": "Domain (e.g. panel.example.com)", + "domainNone": "Using IP address (no custom domain)", + "domainPending": "Setup in progress…", + "domainSetup": "Configure domain", + "domainSetupError": "Failed to start domain setup.", + "domainUnconfigured": "Not configured", + "domainVerify": "Verify DNS", + "domainVerifyError": "Failed to verify.", + "lastUsed": "Last used:", + "migration": "Dashboard Migration", + "migrationAbort": "Abort migration", + "migrationAbortError": "Failed to abort migration.", + "migrationAbortSuccess": "Migration aborted.", + "migrationAgentsProgress": "{confirmed} of {total} agents confirmed", + "migrationConfirmShutdown": "Confirm shutdown", + "migrationConfirmShutdownMsg": "All agents have confirmed. This will shut down the current dashboard. Make sure VPS-B is fully operational. Continue?", + "migrationCopyToken": "Copy", + "migrationDesc": "Move this dashboard to another VPS. Requires a fresh dashboard installation on the target VPS. Agent SSH access may be needed if any agent fails to reconnect automatically.", + "migrationPrepare": "Generate migration token", + "migrationPreparedToken": "Copy this token to VPS-A:", + "migrationPrepareError": "Failed to prepare migration.", + "migrationShutdownError": "Failed to confirm shutdown.", + "migrationSourceDesc": "Enter the target VPS URL and the migration token from VPS-B to start migration.", + "migrationSourceTitle": "Migrate to another VPS (this is VPS-A)", + "migrationStart": "Start migration", + "migrationStartError": "Failed to start migration.", + "migrationStatusAborted": "Aborted", + "migrationStatusCompleted": "Completed", + "migrationStatusError": "Error", + "migrationStatusIdle": "Idle", + "migrationStatusNotifying": "Notifying agents", + "migrationStatusPreparing": "Preparing", + "migrationStatusTransferring": "Transferring", + "migrationStatusWaiting": "Waiting for agents", + "migrationTargetDesc": "Generate a migration token to give to VPS-A.", + "migrationTargetTitle": "Receive migration (this is VPS-B)", + "migrationTargetUrl": "Target VPS URL", + "migrationToken": "Migration token (from VPS-B)", + "newPassword": "New password", + "noSessions": "No active sessions found.", + "profile": "Profile", + "profileUsername": "Username", + "revoke": "Revoke", + "revokeError": "Failed to revoke session.", + "revokeSuccess": "Session revoked.", + "rotateKeys": "Rotate JWT Keys", + "rotateKeysBtn": "Rotate & sign out", + "rotateKeysConfirm": "This will invalidate all active sessions, including yours. Continue?", + "rotateKeysDesc": "Invalidates all active sessions. Everyone must sign in again.", + "rotationLog": "Key rotation history", + "rotationLogEmpty": "No rotations recorded.", + "rotationLogReason": "Reason", + "rotationLogScope": "Scope", + "security": "Security", + "sessions": "Active Sessions", + "singleSession": "Single active session", + "singleSessionDesc": "When enabled, signing in on a new device will invalidate all other active sessions.", + "singleSessionError": "Failed to update setting.", + "singleSessionSuccess": "Single session setting updated.", + "title": "Settings", + "updateAvailable": "Update available", + "updateCheck": "Check for updates", + "updateCheckError": "Failed to check for updates.", + "updateCurrent": "Current:", + "updateLatest": "Latest:", + "updates": "Updates", + "updatesDesc": "Check GitHub Releases for new versions and push updates to all connected agents.", + "updateTrigger": "Trigger update on all agents", + "updateTriggerError": "Failed to trigger update.", + "updateTriggerSuccess": "Update triggered. Agents will update shortly.", + "updateUpToDate": "Up to date" + } + }, + "auth": { + "login": { + "invalidCredentials": "Invalid username or password", + "noAccount": "Don't have an account?", + "password": "Password", + "rateLimited": "Too many attempts. Try again in {minutes} minutes.", + "register": "Create account", + "serverError": "Something went wrong. Please try again.", + "submit": "Sign in", + "submitting": "Signing in...", + "subtitle": "Distributed infrastructure orchestration", + "title": "Sign in to {company}", + "username": "Username" + }, + "register": { + "email": "Email", + "emailTaken": "Email already registered", + "hasAccount": "Already have an account?", + "login": "Sign in", + "password": "Password", + "serverError": "Something went wrong. Please try again.", + "submit": "Create account", + "submitting": "Creating account...", + "subtitle": "Start orchestrating your infrastructure", + "title": "Create your account", + "username": "Username", + "usernameTaken": "Username already taken", + "validation": { + "emailInvalid": "Enter a valid email address", + "passwordLowercase": "Must contain at least one lowercase letter", + "passwordMax": "Password cannot exceed 30 characters", + "passwordMin": "Password must be at least 12 characters", + "passwordNumber": "Must contain at least one number", + "passwordSpecial": "Must contain at least one special character", + "passwordUppercase": "Must contain at least one uppercase letter", + "usernameChars": "Only lowercase letters, numbers, - and _ allowed", + "usernameEdge": "Cannot start or end with - or _", + "usernameMax": "Username cannot exceed 32 characters", + "usernameMin": "Username must be at least 3 characters", + "usernameReserved": "This username is reserved" + } + } + }, + "branding": { + "author": "Jaroc", + "madeWith": "Made with love by" + } } diff --git a/lynx/dashboard/ui/messages/es.json b/lynx/dashboard/ui/messages/es.json index 8100532..de329bf 100644 --- a/lynx/dashboard/ui/messages/es.json +++ b/lynx/dashboard/ui/messages/es.json @@ -1,417 +1,417 @@ { - "auth": { - "login": { - "title": "Iniciar sesión en {company}", - "subtitle": "Orquestación de infraestructura distribuida", - "username": "Usuario", - "password": "Contraseña", - "submit": "Iniciar sesión", - "submitting": "Iniciando sesión...", - "noAccount": "¿No tienes cuenta?", - "register": "Crear cuenta", - "invalidCredentials": "Usuario o contraseña inválidos", - "rateLimited": "Demasiados intentos. Vuelve a intentarlo en {minutes} minutos.", - "serverError": "Algo salió mal. Por favor inténtalo de nuevo." - }, - "register": { - "title": "Crear tu cuenta", - "subtitle": "Empieza a orquestar tu infraestructura", - "username": "Usuario", - "email": "Correo electrónico", - "password": "Contraseña", - "submit": "Crear cuenta", - "submitting": "Creando cuenta...", - "hasAccount": "¿Ya tienes cuenta?", - "login": "Iniciar sesión", - "usernameTaken": "Nombre de usuario ya en uso", - "emailTaken": "Correo electrónico ya registrado", - "serverError": "Algo salió mal. Por favor inténtalo de nuevo.", - "validation": { - "usernameMin": "El usuario debe tener al menos 3 caracteres", - "usernameMax": "El usuario no puede superar 32 caracteres", - "usernameChars": "Solo se permiten letras minúsculas, números, - y _", - "usernameEdge": "No puede comenzar ni terminar con - o _", - "usernameReserved": "Este nombre de usuario está reservado", - "emailInvalid": "Introduce una dirección de correo válida", - "passwordMin": "La contraseña debe tener al menos 12 caracteres", - "passwordMax": "La contraseña no puede superar 30 caracteres", - "passwordUppercase": "Debe contener al menos una letra mayúscula", - "passwordLowercase": "Debe contener al menos una letra minúscula", - "passwordNumber": "Debe contener al menos un número", - "passwordSpecial": "Debe contener al menos un carácter especial" - } - } - }, - "app": { - "nav": { - "overview": "Resumen", - "agents": "Agentes", - "organizations": "Organizaciones", - "settings": "Ajustes", - "admin": "Admin", - "signOut": "Cerrar sesión" - }, - "organizations": { - "title": "Organizaciones", - "create": "Nueva organización", - "slugConflict": "Slug ya en uso. Elige un nombre diferente.", - "createError": "Error al crear la organización.", - "noOrgs": "Sin organizaciones aún", - "noOrgsDesc": "Crea una organización para empezar a gestionar proyectos y contenedores.", - "slug": "Slug:", - "members": "Miembros:", - "invite": "Invitar miembro", - "inviteTitle": "Invitar un miembro", - "inviteUsername": "Usuario", - "inviteRole": "Rol", - "inviteSubmit": "Enviar invitación", - "inviteSuccess": "Miembro invitado.", - "inviteError": "Error al invitar miembro.", - "removeMember": "Eliminar", - "removeMemberSuccess": "Miembro eliminado.", - "removeMemberError": "Error al eliminar miembro.", - "noMembers": "Sin miembros aún.", - "orgId": "ID:", - "projects": "Proyectos", - "noProjects": "Sin proyectos aún.", - "createProject": "Nuevo proyecto", - "createProjectTitle": "Crear proyecto", - "projectName": "Nombre", - "projectSlug": "Slug", - "projectAgent": "Agente destino", - "projectNoAgents": "No hay agentes disponibles. Registra un agente primero.", - "projectCreate": "Crear proyecto", - "projectCreateSuccess": "Proyecto creado.", - "projectSlugConflict": "Slug ya en uso en esta organización.", - "projectCreateError": "Error al crear el proyecto." - }, - "projects": { - "orgs": "Organizaciones", - "org": "Organización", - "slug": "Slug:", - "verticalScale": "Escalado vertical", - "verticalScaleDesc": "Actualiza los límites de CPU y memoria de un contenedor en este proyecto. El contenedor se actualiza en vivo sin reinicio cuando es posible.", - "containerName": "Nombre del contenedor", - "cpus": "CPUs", - "memoryMb": "Memoria (MB)", - "apply": "Aplicar límites", - "applySuccess": "Límites del contenedor actualizados.", - "applyError": "Error al actualizar los límites del contenedor.", - "projectId": "ID del proyecto:", - "containers": "Contenedores", - "noContainers": "Sin contenedores en ejecución.", - "cStart": "Iniciar", - "cStop": "Detener", - "cRestart": "Reiniciar", - "cRemove": "Eliminar", - "cActionSuccess": "hecho", - "cActionError": "Acción de contenedor fallida.", - "deploy": "Desplegar contenedor", - "deployBtn": "Desplegar", - "deploySuccess": "Contenedor desplegado.", - "deployError": "Error al desplegar.", - "cName": "Nombre del contenedor", - "cImage": "Imagen", - "cPorts": "Puertos (separados por coma)", - "cEnv": "Variables de entorno (separadas por coma)", - "horizontalScale": "Escalado horizontal", - "horizontalScaleDesc": "Despliega réplicas en otro agente mediante un tunnel WireGuard directo de plano de datos. El tráfico va directo entre agentes, nunca pasa por el dashboard.", - "addTunnel": "Añadir tunnel", - "addTunnelTitle": "Escalar a otro agente", - "tunnelTargetAgent": "Agente destino", - "tunnelImage": "Imagen del contenedor", - "tunnelReplicas": "Número de réplicas", - "tunnelConfirm": "Establecer tunnel y desplegar", - "tunnelSuccess": "Tunnel establecido y réplicas desplegadas.", - "tunnelError": "Error al establecer el tunnel.", - "tunnelTeardownSuccess": "Tunnel eliminado.", - "tunnelTeardownError": "Error al eliminar el tunnel.", - "noTunnels": "Sin tunnels activos.", - "tunnelReplicaCount": "Réplicas" - }, - "settings": { - "title": "Ajustes", - "profile": "Perfil", - "profileUsername": "Usuario", - "changePassword": "Cambiar contraseña", - "currentPassword": "Contraseña actual", - "newPassword": "Nueva contraseña", - "changePasswordBtn": "Cambiar contraseña", - "changePasswordSuccess": "Contraseña cambiada. Has cerrado sesión.", - "changePasswordWrong": "La contraseña actual es incorrecta.", - "changePasswordError": "Error al cambiar la contraseña.", - "singleSession": "Sesión única activa", - "singleSessionDesc": "Al activarlo, iniciar sesión en un nuevo dispositivo invalidará todas las demás sesiones activas.", - "singleSessionSuccess": "Configuración de sesión única actualizada.", - "singleSessionError": "Error al actualizar la configuración.", - "domain": "Dominio", - "domainDesc": "Configura un dominio personalizado con TLS de Let's Encrypt. Requiere registro DNS A apuntando a este servidor antes de la configuración.", - "domainCurrent": "Dominio actual:", - "domainNone": "Usando dirección IP (sin dominio personalizado)", - "domainInput": "Dominio (ej: panel.ejemplo.com)", - "domainEmail": "Email para Let's Encrypt", - "domainSetup": "Configurar dominio", - "domainPending": "Configuración en progreso…", - "domainActive": "Activo", - "domainError": "Error", - "domainUnconfigured": "Sin configurar", - "domainVerify": "Verificar DNS", - "domainDnsOk": "DNS resuelto correctamente.", - "domainDnsFail": "El dominio no resuelve. Comprueba tu registro DNS A.", - "domainVerifyError": "Error al verificar.", - "domainSetupError": "Error al iniciar la configuración del dominio.", - "domainHsts": "HSTS", - "domainHstsDesc": "Fuerza HTTPS para todos los visitantes. Activar solo después de confirmar dominio y cert TLS funcionando.", - "domainHstsEnable": "Activar HSTS", - "domainHstsDisable": "Desactivar HSTS", - "domainHstsSuccess": "HSTS actualizado.", - "domainHstsError": "Error al actualizar HSTS.", - "domainClosePort": "Cerrar puerto 19443", - "domainClosePortDesc": "Elimina el acceso directo por IP en el puerto 19443. Solo posible cuando el dominio está activo. Requiere SSH para reactivar.", - "domainClosePortBtn": "Cerrar puerto 19443", - "domainClosePortConfirm": "Esto eliminará el acceso en el puerto 19443. Solo podrás acceder al panel desde tu dominio configurado. No se puede deshacer remotamente. ¿Continuar?", - "domainClosePortSuccess": "Puerto 19443 cerrado.", - "domainClosePortError": "Error al cerrar el puerto.", - "domainCert": "Certificado:", - "domainCertSelfSigned": "Autofirmado", - "domainCertLE": "Let's Encrypt", - "domainCertCloudflare": "Cloudflare Origin", - "domainCertCustom": "Personalizado", - "domainCertExpires": "Expira:", - "domainCertUpload": "Subir certificado", - "domainCertUploadCloudflare": "Cloudflare Origin", - "domainCertUploadCustom": "Certificado personalizado", - "domainCertPem": "Certificado (PEM)", - "domainCertPemPlaceholder": "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----", - "domainCertKeyPem": "Clave privada (PEM)", - "domainCertKeyPemPlaceholder": "-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----", - "domainCertKeyOptional": "La clave privada no es necesaria para certificados Cloudflare Origin.", - "domainCertUploadSuccess": "Certificado subido y aplicado.", - "domainCertUploadError": "Error al subir el certificado.", - "security": "Seguridad", - "rotationLog": "Historial de rotación de claves", - "rotationLogEmpty": "Sin rotaciones registradas.", - "rotationLogReason": "Motivo", - "rotationLogScope": "Ámbito", - "rotateKeys": "Rotar claves JWT", - "rotateKeysDesc": "Invalida todas las sesiones activas. Todos deben iniciar sesión de nuevo.", - "rotateKeysBtn": "Rotar y cerrar sesión", - "rotateKeysConfirm": "Esto invalidará todas las sesiones activas, incluida la tuya. ¿Continuar?", - "sessions": "Sesiones activas", - "noSessions": "No se encontraron sesiones activas.", - "lastUsed": "Último uso:", - "revoke": "Revocar", - "revokeSuccess": "Sesión revocada.", - "revokeError": "Error al revocar la sesión.", - "updates": "Actualizaciones", - "updatesDesc": "Consulta GitHub Releases para nuevas versiones y envía actualizaciones a todos los agentes conectados.", - "updateCheck": "Buscar actualizaciones", - "updateCurrent": "Actual:", - "updateLatest": "Última:", - "updateUpToDate": "Al día", - "updateAvailable": "Actualización disponible", - "updateTrigger": "Actualizar todos los agentes", - "updateTriggerSuccess": "Actualización iniciada. Los agentes actualizarán pronto.", - "updateTriggerError": "Error al iniciar la actualización.", - "updateCheckError": "Error al buscar actualizaciones.", - "migration": "Migración del dashboard", - "migrationDesc": "Mueve este dashboard a otro VPS. Requiere instalación fresca del dashboard en el VPS destino. Puede requerirse acceso SSH al agente si alguno no reconecta automáticamente.", - "migrationSourceTitle": "Migrar a otro VPS (este es VPS-A)", - "migrationSourceDesc": "Introduce la URL del VPS destino y el token de migración del VPS-B para iniciar la migración.", - "migrationTargetUrl": "URL del VPS destino", - "migrationToken": "Token de migración (del VPS-B)", - "migrationStart": "Iniciar migración", - "migrationTargetTitle": "Recibir migración (este es VPS-B)", - "migrationTargetDesc": "Genera un token de migración para dar al VPS-A.", - "migrationPrepare": "Generar token de migración", - "migrationPreparedToken": "Copia este token en VPS-A:", - "migrationCopyToken": "Copiar", - "migrationAbort": "Abortar migración", - "migrationConfirmShutdown": "Confirmar apagado", - "migrationConfirmShutdownMsg": "Todos los agentes han confirmado. Esto apagará el dashboard actual. Asegúrate de que el VPS-B esté completamente operativo. ¿Continuar?", - "migrationStatusIdle": "Inactivo", - "migrationStatusPreparing": "Preparando", - "migrationStatusTransferring": "Transfiriendo", - "migrationStatusNotifying": "Notificando agentes", - "migrationStatusWaiting": "Esperando agentes", - "migrationStatusCompleted": "Completado", - "migrationStatusAborted": "Abortado", - "migrationStatusError": "Error", - "migrationAgentsProgress": "{confirmed} de {total} agentes confirmados", - "migrationPrepareError": "Error al preparar la migración.", - "migrationStartError": "Error al iniciar la migración.", - "migrationAbortSuccess": "Migración abortada.", - "migrationAbortError": "Error al abortar la migración.", - "migrationShutdownError": "Error al confirmar el apagado.", - "branding": "Marca blanca", - "brandingCompanyName": "Nombre de empresa", - "brandingLogoUrl": "URL del logotipo", - "brandingPrimaryColor": "Primario", - "brandingSecondaryColor": "Secundario", - "brandingAccentColor": "Acento", - "brandingSave": "Guardar marca", - "brandingSaved": "Marca actualizada.", - "brandingError": "Error al actualizar la marca." - }, - "auditLog": { - "title": "Registro de auditoría", - "noEntries": "Sin entradas de auditoría aún.", - "command": "Comando", - "result": "Resultado", - "user": "Usuario", - "org": "Org", - "time": "Hora", - "hash": "Hash", - "error": "Error", - "loadMore": "Cargar más", - "back": "Volver a agentes", - "resultSuccess": "éxito", - "resultRejected": "rechazado", - "resultFailed": "fallido" - }, - "agents": { - "title": "Agentes", - "register": "Registrar agente", - "registerSuccess": "Agente registrado", - "registerSuccessDesc": "Copia las credenciales. El token de sincronización solo se muestra una vez.", - "agentId": "ID del agente", - "wgIp": "IP WireGuard:", - "syncToken": "Token de sincronización", - "warnOnce": "El token de sincronización no volverá a mostrarse. Cópialo ahora.", - "done": "Hecho", - "noAgents": "No hay agentes conectados", - "noAgentsDesc": "Ejecuta el script de instalación del agente en un VPS para conectarlo.", - "nftDiverged": "Ruleset de nftables modificado fuera de Lynx", - "nftRestore": "Restaurar reglas Lynx", - "nftAccept": "Aceptar cambios", - "nftRestoreSuccess": "Ruleset de nftables restaurado.", - "nftAcceptSuccess": "Cambios manuales aceptados.", - "nftError": "Error al resolver divergencia de nftables.", - "auditLog": "Registro de auditoría", - "version": "Versión:", - "lastHeartbeat": "Último latido:", - "wgIpLabel": "IP WireGuard:", - "name": "Nombre:", - "notFound": "Agente no encontrado.", - "backToAgents": "← Agentes", - "detailTitle": "Detalle del agente", - "reboot": "Reiniciar VPS", - "rebootConfirm": "Esto reiniciará el VPS. El agente se reconectará automáticamente tras el arranque. ¿Continuar?", - "rebootSuccess": "Comando de reinicio enviado.", - "rebootError": "Error al enviar el comando de reinicio.", - "deleteAgent": "Eliminar agente", - "deleteConfirm": "¿Eliminar este agente del dashboard? El agente perderá su conexión WireGuard. ¿Continuar?", - "deleteSuccess": "Agente eliminado.", - "deleteError": "Error al eliminar el agente.", - "nftRules": "Reglas de firewall", - "nftRulesDesc": "Reglas nftables aplicadas a este agente. Los cambios se envían automáticamente.", - "nftGlobal": "Reglas globales", - "nftLocal": "Reglas locales (este agente)", - "nftStatus": "Estado", - "nftChecksumOk": "Sincronizado", - "nftChecksumMismatch": "Divergido", - "nftAddRule": "Añadir regla", - "nftKind": "Tipo", - "nftPort": "Puerto", - "nftProtocol": "Protocolo", - "nftIpList": "Lista de IPs (CIDRs, separados por coma)", - "nftRatePerMin": "Conexiones por minuto", - "nftDescription": "Descripción (opcional)", - "nftPriority": "Prioridad", - "nftCreate": "Crear regla", - "nftCreateSuccess": "Regla creada.", - "nftCreateError": "Error al crear la regla.", - "nftDeleteSuccess": "Regla eliminada.", - "nftDeleteError": "Error al eliminar la regla.", - "nftPush": "Enviar al agente", - "nftPushSuccess": "Reglas enviadas.", - "nftPushError": "Error al enviar las reglas.", - "nftNoRules": "Sin reglas definidas.", - "nftKindAllowPort": "Permitir puerto", - "nftKindBlockPort": "Bloquear puerto", - "nftKindAllowIp": "Permitir IP", - "nftKindBlockIp": "Bloquear IP", - "nftKindRateLimit": "Limitar tasa", - "nftProtoTcp": "TCP", - "nftProtoUdp": "UDP", - "nftProtoBoth": "TCP + UDP", - "metricsLive": "Métricas en vivo", - "metricsCpu": "CPU", - "metricsMemory": "Memoria", - "metricsDisk": "Disco", - "metricsConnecting": "Conectando al agente...", - "metricsAgentOffline": "Agente desconectado", - "metricsOffline": "Métricas no disponibles", - "status": { - "online": "En línea", - "lockdown": "Bloqueado", - "offline": "Desconectado" - } - }, - "notifications": { - "label": "Notificaciones", - "title": "Notificaciones", - "clearAll": "Limpiar todo", - "empty": "Sin alertas", - "event": { - "heartbeat_lost": "Latido perdido", - "lockdown": "Agente en bloqueo", - "nftables_divergence": "Divergencia nftables", - "conflicting_software_detected": "Software incompatible" - } - }, - "overview": { - "title": "Resumen", - "agentsOnline": "Agentes en línea", - "organizations": "Organizaciones", - "recentEvents": "Eventos recientes", - "noEvents": "Sin eventos recientes.", - "noAgents": "No hay agentes conectados", - "noAgentsDesc": "Conecta un VPS agente para empezar a gestionar tu infraestructura.", - "securityAlerts": "Alertas de seguridad", - "noAlerts": "Sin alertas activas.", - "acknowledge": "Descartar", - "acknowledged": "Alerta descartada.", - "acknowledgeError": "Error al descartar la alerta." - } - }, - "branding": { - "madeWith": "Hecho con amor por", - "author": "Jaroc" - }, - "admin": { - "title": "Admin", - "users": "Usuarios", - "roles": "Roles", - "deleteUser": "Eliminar usuario", - "deleteConfirm": "Eliminar usuario", - "deleteSuccess": "Usuario eliminado.", - "deleteError": "Error al eliminar usuario.", - "forcePasswordChange": "Forzar restablecimiento de contraseña", - "forcePasswordChangeSuccess": "Restablecimiento forzado.", - "forcePasswordChangeError": "Error al forzar restablecimiento.", - "addRole": "Asignar rol", - "addRoleSuccess": "Rol asignado.", - "addRoleError": "Error al asignar rol.", - "removeRole": "Quitar rol", - "removeRoleSuccess": "Rol eliminado.", - "removeRoleError": "Error al quitar rol.", - "noRoles": "Sin roles asignados.", - "selectRole": "Seleccionar rol", - "createRole": "Nuevo rol", - "createRoleSuccess": "Rol creado.", - "createRoleError": "Error al crear rol.", - "deleteRole": "Eliminar rol", - "deleteRoleConfirm": "Eliminar rol", - "deleteRoleSuccess": "Rol eliminado.", - "deleteRoleError": "Error al eliminar rol.", - "addPermission": "Añadir permiso", - "addPermissionSuccess": "Permiso añadido.", - "addPermissionError": "Error al añadir permiso.", - "removePermission": "Quitar permiso", - "removePermissionSuccess": "Permiso eliminado.", - "removePermissionError": "Error al quitar permiso.", - "roleName": "Nombre del rol", - "noPermissions": "Sin permisos asignados." - } + "admin": { + "addPermission": "Añadir permiso", + "addPermissionError": "Error al añadir permiso.", + "addPermissionSuccess": "Permiso añadido.", + "addRole": "Asignar rol", + "addRoleError": "Error al asignar rol.", + "addRoleSuccess": "Rol asignado.", + "createRole": "Nuevo rol", + "createRoleError": "Error al crear rol.", + "createRoleSuccess": "Rol creado.", + "deleteConfirm": "Eliminar usuario", + "deleteError": "Error al eliminar usuario.", + "deleteRole": "Eliminar rol", + "deleteRoleConfirm": "Eliminar rol", + "deleteRoleError": "Error al eliminar rol.", + "deleteRoleSuccess": "Rol eliminado.", + "deleteSuccess": "Usuario eliminado.", + "deleteUser": "Eliminar usuario", + "forcePasswordChange": "Forzar restablecimiento de contraseña", + "forcePasswordChangeError": "Error al forzar restablecimiento.", + "forcePasswordChangeSuccess": "Restablecimiento forzado.", + "noPermissions": "Sin permisos asignados.", + "noRoles": "Sin roles asignados.", + "removePermission": "Quitar permiso", + "removePermissionError": "Error al quitar permiso.", + "removePermissionSuccess": "Permiso eliminado.", + "removeRole": "Quitar rol", + "removeRoleError": "Error al quitar rol.", + "removeRoleSuccess": "Rol eliminado.", + "roleName": "Nombre del rol", + "roles": "Roles", + "selectRole": "Seleccionar rol", + "title": "Admin", + "users": "Usuarios" + }, + "app": { + "agents": { + "agentId": "ID del agente", + "auditLog": "Registro de auditoría", + "backToAgents": "← Agentes", + "deleteAgent": "Eliminar agente", + "deleteConfirm": "¿Eliminar este agente del dashboard? El agente perderá su conexión WireGuard. ¿Continuar?", + "deleteError": "Error al eliminar el agente.", + "deleteSuccess": "Agente eliminado.", + "detailTitle": "Detalle del agente", + "done": "Hecho", + "lastHeartbeat": "Último latido:", + "metricsAgentOffline": "Agente desconectado", + "metricsConnecting": "Conectando al agente...", + "metricsCpu": "CPU", + "metricsDisk": "Disco", + "metricsLive": "Métricas en vivo", + "metricsMemory": "Memoria", + "metricsOffline": "Métricas no disponibles", + "name": "Nombre:", + "nftAccept": "Aceptar cambios", + "nftAcceptSuccess": "Cambios manuales aceptados.", + "nftAddRule": "Añadir regla", + "nftChecksumMismatch": "Divergido", + "nftChecksumOk": "Sincronizado", + "nftCreate": "Crear regla", + "nftCreateError": "Error al crear la regla.", + "nftCreateSuccess": "Regla creada.", + "nftDeleteError": "Error al eliminar la regla.", + "nftDeleteSuccess": "Regla eliminada.", + "nftDescription": "Descripción (opcional)", + "nftDiverged": "Ruleset de nftables modificado fuera de Lynx", + "nftError": "Error al resolver divergencia de nftables.", + "nftGlobal": "Reglas globales", + "nftIpList": "Lista de IPs (CIDRs, separados por coma)", + "nftKind": "Tipo", + "nftKindAllowIp": "Permitir IP", + "nftKindAllowPort": "Permitir puerto", + "nftKindBlockIp": "Bloquear IP", + "nftKindBlockPort": "Bloquear puerto", + "nftKindRateLimit": "Limitar tasa", + "nftLocal": "Reglas locales (este agente)", + "nftNoRules": "Sin reglas definidas.", + "nftPort": "Puerto", + "nftPriority": "Prioridad", + "nftProtoBoth": "TCP + UDP", + "nftProtocol": "Protocolo", + "nftProtoTcp": "TCP", + "nftProtoUdp": "UDP", + "nftPush": "Enviar al agente", + "nftPushError": "Error al enviar las reglas.", + "nftPushSuccess": "Reglas enviadas.", + "nftRatePerMin": "Conexiones por minuto", + "nftRestore": "Restaurar reglas Lynx", + "nftRestoreSuccess": "Ruleset de nftables restaurado.", + "nftRules": "Reglas de firewall", + "nftRulesDesc": "Reglas nftables aplicadas a este agente. Los cambios se envían automáticamente.", + "nftStatus": "Estado", + "noAgents": "No hay agentes conectados", + "noAgentsDesc": "Ejecuta el script de instalación del agente en un VPS para conectarlo.", + "notFound": "Agente no encontrado.", + "reboot": "Reiniciar VPS", + "rebootConfirm": "Esto reiniciará el VPS. El agente se reconectará automáticamente tras el arranque. ¿Continuar?", + "rebootError": "Error al enviar el comando de reinicio.", + "rebootSuccess": "Comando de reinicio enviado.", + "register": "Registrar agente", + "registerSuccess": "Agente registrado", + "registerSuccessDesc": "Copia las credenciales. El token de sincronización solo se muestra una vez.", + "status": { + "lockdown": "Bloqueado", + "offline": "Desconectado", + "online": "En línea" + }, + "syncToken": "Token de sincronización", + "title": "Agentes", + "version": "Versión:", + "warnOnce": "El token de sincronización no volverá a mostrarse. Cópialo ahora.", + "wgIp": "IP WireGuard:", + "wgIpLabel": "IP WireGuard:" + }, + "auditLog": { + "back": "Volver a agentes", + "command": "Comando", + "error": "Error", + "hash": "Hash", + "loadMore": "Cargar más", + "noEntries": "Sin entradas de auditoría aún.", + "org": "Org", + "result": "Resultado", + "resultFailed": "fallido", + "resultRejected": "rechazado", + "resultSuccess": "éxito", + "time": "Hora", + "title": "Registro de auditoría", + "user": "Usuario" + }, + "nav": { + "admin": "Admin", + "agents": "Agentes", + "organizations": "Organizaciones", + "overview": "Resumen", + "settings": "Ajustes", + "signOut": "Cerrar sesión" + }, + "notifications": { + "clearAll": "Limpiar todo", + "empty": "Sin alertas", + "event": { + "conflicting_software_detected": "Software incompatible", + "heartbeat_lost": "Latido perdido", + "lockdown": "Agente en bloqueo", + "nftables_divergence": "Divergencia nftables" + }, + "label": "Notificaciones", + "title": "Notificaciones" + }, + "organizations": { + "create": "Nueva organización", + "createError": "Error al crear la organización.", + "createProject": "Nuevo proyecto", + "createProjectTitle": "Crear proyecto", + "invite": "Invitar miembro", + "inviteError": "Error al invitar miembro.", + "inviteRole": "Rol", + "inviteSubmit": "Enviar invitación", + "inviteSuccess": "Miembro invitado.", + "inviteTitle": "Invitar un miembro", + "inviteUsername": "Usuario", + "members": "Miembros:", + "noMembers": "Sin miembros aún.", + "noOrgs": "Sin organizaciones aún", + "noOrgsDesc": "Crea una organización para empezar a gestionar proyectos y contenedores.", + "noProjects": "Sin proyectos aún.", + "orgId": "ID:", + "projectAgent": "Agente destino", + "projectCreate": "Crear proyecto", + "projectCreateError": "Error al crear el proyecto.", + "projectCreateSuccess": "Proyecto creado.", + "projectName": "Nombre", + "projectNoAgents": "No hay agentes disponibles. Registra un agente primero.", + "projectSlug": "Slug", + "projectSlugConflict": "Slug ya en uso en esta organización.", + "projects": "Proyectos", + "removeMember": "Eliminar", + "removeMemberError": "Error al eliminar miembro.", + "removeMemberSuccess": "Miembro eliminado.", + "slug": "Slug:", + "slugConflict": "Slug ya en uso. Elige un nombre diferente.", + "title": "Organizaciones" + }, + "overview": { + "acknowledge": "Descartar", + "acknowledged": "Alerta descartada.", + "acknowledgeError": "Error al descartar la alerta.", + "agentsOnline": "Agentes en línea", + "noAgents": "No hay agentes conectados", + "noAgentsDesc": "Conecta un VPS agente para empezar a gestionar tu infraestructura.", + "noAlerts": "Sin alertas activas.", + "noEvents": "Sin eventos recientes.", + "organizations": "Organizaciones", + "recentEvents": "Eventos recientes", + "securityAlerts": "Alertas de seguridad", + "title": "Resumen" + }, + "projects": { + "addTunnel": "Añadir tunnel", + "addTunnelTitle": "Escalar a otro agente", + "apply": "Aplicar límites", + "applyError": "Error al actualizar los límites del contenedor.", + "applySuccess": "Límites del contenedor actualizados.", + "cActionError": "Acción de contenedor fallida.", + "cActionSuccess": "hecho", + "cEnv": "Variables de entorno (separadas por coma)", + "cImage": "Imagen", + "cName": "Nombre del contenedor", + "containerName": "Nombre del contenedor", + "containers": "Contenedores", + "cPorts": "Puertos (separados por coma)", + "cpus": "CPUs", + "cRemove": "Eliminar", + "cRestart": "Reiniciar", + "cStart": "Iniciar", + "cStop": "Detener", + "deploy": "Desplegar contenedor", + "deployBtn": "Desplegar", + "deployError": "Error al desplegar.", + "deploySuccess": "Contenedor desplegado.", + "horizontalScale": "Escalado horizontal", + "horizontalScaleDesc": "Despliega réplicas en otro agente mediante un tunnel WireGuard directo de plano de datos. El tráfico va directo entre agentes, nunca pasa por el dashboard.", + "memoryMb": "Memoria (MB)", + "noContainers": "Sin contenedores en ejecución.", + "noTunnels": "Sin tunnels activos.", + "org": "Organización", + "orgs": "Organizaciones", + "projectId": "ID del proyecto:", + "slug": "Slug:", + "tunnelConfirm": "Establecer tunnel y desplegar", + "tunnelError": "Error al establecer el tunnel.", + "tunnelImage": "Imagen del contenedor", + "tunnelReplicaCount": "Réplicas", + "tunnelReplicas": "Número de réplicas", + "tunnelSuccess": "Tunnel establecido y réplicas desplegadas.", + "tunnelTargetAgent": "Agente destino", + "tunnelTeardownError": "Error al eliminar el tunnel.", + "tunnelTeardownSuccess": "Tunnel eliminado.", + "verticalScale": "Escalado vertical", + "verticalScaleDesc": "Actualiza los límites de CPU y memoria de un contenedor en este proyecto. El contenedor se actualiza en vivo sin reinicio cuando es posible." + }, + "settings": { + "branding": "Marca blanca", + "brandingAccentColor": "Acento", + "brandingCompanyName": "Nombre de empresa", + "brandingError": "Error al actualizar la marca.", + "brandingLogoUrl": "URL del logotipo", + "brandingPrimaryColor": "Primario", + "brandingSave": "Guardar marca", + "brandingSaved": "Marca actualizada.", + "brandingSecondaryColor": "Secundario", + "changePassword": "Cambiar contraseña", + "changePasswordBtn": "Cambiar contraseña", + "changePasswordError": "Error al cambiar la contraseña.", + "changePasswordSuccess": "Contraseña cambiada. Has cerrado sesión.", + "changePasswordWrong": "La contraseña actual es incorrecta.", + "currentPassword": "Contraseña actual", + "domain": "Dominio", + "domainActive": "Activo", + "domainCert": "Certificado:", + "domainCertCloudflare": "Cloudflare Origin", + "domainCertCustom": "Personalizado", + "domainCertExpires": "Expira:", + "domainCertKeyOptional": "La clave privada no es necesaria para certificados Cloudflare Origin.", + "domainCertKeyPem": "Clave privada (PEM)", + "domainCertKeyPemPlaceholder": "-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----", + "domainCertLE": "Let's Encrypt", + "domainCertPem": "Certificado (PEM)", + "domainCertPemPlaceholder": "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----", + "domainCertSelfSigned": "Autofirmado", + "domainCertUpload": "Subir certificado", + "domainCertUploadCloudflare": "Cloudflare Origin", + "domainCertUploadCustom": "Certificado personalizado", + "domainCertUploadError": "Error al subir el certificado.", + "domainCertUploadSuccess": "Certificado subido y aplicado.", + "domainClosePort": "Cerrar puerto 19443", + "domainClosePortBtn": "Cerrar puerto 19443", + "domainClosePortConfirm": "Esto eliminará el acceso en el puerto 19443. Solo podrás acceder al panel desde tu dominio configurado. No se puede deshacer remotamente. ¿Continuar?", + "domainClosePortDesc": "Elimina el acceso directo por IP en el puerto 19443. Solo posible cuando el dominio está activo. Requiere SSH para reactivar.", + "domainClosePortError": "Error al cerrar el puerto.", + "domainClosePortSuccess": "Puerto 19443 cerrado.", + "domainCurrent": "Dominio actual:", + "domainDesc": "Configura un dominio personalizado con TLS de Let's Encrypt. Requiere registro DNS A apuntando a este servidor antes de la configuración.", + "domainDnsFail": "El dominio no resuelve. Comprueba tu registro DNS A.", + "domainDnsOk": "DNS resuelto correctamente.", + "domainEmail": "Email para Let's Encrypt", + "domainError": "Error", + "domainHsts": "HSTS", + "domainHstsDesc": "Fuerza HTTPS para todos los visitantes. Activar solo después de confirmar dominio y cert TLS funcionando.", + "domainHstsDisable": "Desactivar HSTS", + "domainHstsEnable": "Activar HSTS", + "domainHstsError": "Error al actualizar HSTS.", + "domainHstsSuccess": "HSTS actualizado.", + "domainInput": "Dominio (ej: panel.ejemplo.com)", + "domainNone": "Usando dirección IP (sin dominio personalizado)", + "domainPending": "Configuración en progreso…", + "domainSetup": "Configurar dominio", + "domainSetupError": "Error al iniciar la configuración del dominio.", + "domainUnconfigured": "Sin configurar", + "domainVerify": "Verificar DNS", + "domainVerifyError": "Error al verificar.", + "lastUsed": "Último uso:", + "migration": "Migración del dashboard", + "migrationAbort": "Abortar migración", + "migrationAbortError": "Error al abortar la migración.", + "migrationAbortSuccess": "Migración abortada.", + "migrationAgentsProgress": "{confirmed} de {total} agentes confirmados", + "migrationConfirmShutdown": "Confirmar apagado", + "migrationConfirmShutdownMsg": "Todos los agentes han confirmado. Esto apagará el dashboard actual. Asegúrate de que el VPS-B esté completamente operativo. ¿Continuar?", + "migrationCopyToken": "Copiar", + "migrationDesc": "Mueve este dashboard a otro VPS. Requiere instalación fresca del dashboard en el VPS destino. Puede requerirse acceso SSH al agente si alguno no reconecta automáticamente.", + "migrationPrepare": "Generar token de migración", + "migrationPreparedToken": "Copia este token en VPS-A:", + "migrationPrepareError": "Error al preparar la migración.", + "migrationShutdownError": "Error al confirmar el apagado.", + "migrationSourceDesc": "Introduce la URL del VPS destino y el token de migración del VPS-B para iniciar la migración.", + "migrationSourceTitle": "Migrar a otro VPS (este es VPS-A)", + "migrationStart": "Iniciar migración", + "migrationStartError": "Error al iniciar la migración.", + "migrationStatusAborted": "Abortado", + "migrationStatusCompleted": "Completado", + "migrationStatusError": "Error", + "migrationStatusIdle": "Inactivo", + "migrationStatusNotifying": "Notificando agentes", + "migrationStatusPreparing": "Preparando", + "migrationStatusTransferring": "Transfiriendo", + "migrationStatusWaiting": "Esperando agentes", + "migrationTargetDesc": "Genera un token de migración para dar al VPS-A.", + "migrationTargetTitle": "Recibir migración (este es VPS-B)", + "migrationTargetUrl": "URL del VPS destino", + "migrationToken": "Token de migración (del VPS-B)", + "newPassword": "Nueva contraseña", + "noSessions": "No se encontraron sesiones activas.", + "profile": "Perfil", + "profileUsername": "Usuario", + "revoke": "Revocar", + "revokeError": "Error al revocar la sesión.", + "revokeSuccess": "Sesión revocada.", + "rotateKeys": "Rotar claves JWT", + "rotateKeysBtn": "Rotar y cerrar sesión", + "rotateKeysConfirm": "Esto invalidará todas las sesiones activas, incluida la tuya. ¿Continuar?", + "rotateKeysDesc": "Invalida todas las sesiones activas. Todos deben iniciar sesión de nuevo.", + "rotationLog": "Historial de rotación de claves", + "rotationLogEmpty": "Sin rotaciones registradas.", + "rotationLogReason": "Motivo", + "rotationLogScope": "Ámbito", + "security": "Seguridad", + "sessions": "Sesiones activas", + "singleSession": "Sesión única activa", + "singleSessionDesc": "Al activarlo, iniciar sesión en un nuevo dispositivo invalidará todas las demás sesiones activas.", + "singleSessionError": "Error al actualizar la configuración.", + "singleSessionSuccess": "Configuración de sesión única actualizada.", + "title": "Ajustes", + "updateAvailable": "Actualización disponible", + "updateCheck": "Buscar actualizaciones", + "updateCheckError": "Error al buscar actualizaciones.", + "updateCurrent": "Actual:", + "updateLatest": "Última:", + "updates": "Actualizaciones", + "updatesDesc": "Consulta GitHub Releases para nuevas versiones y envía actualizaciones a todos los agentes conectados.", + "updateTrigger": "Actualizar todos los agentes", + "updateTriggerError": "Error al iniciar la actualización.", + "updateTriggerSuccess": "Actualización iniciada. Los agentes actualizarán pronto.", + "updateUpToDate": "Al día" + } + }, + "auth": { + "login": { + "invalidCredentials": "Usuario o contraseña inválidos", + "noAccount": "¿No tienes cuenta?", + "password": "Contraseña", + "rateLimited": "Demasiados intentos. Vuelve a intentarlo en {minutes} minutos.", + "register": "Crear cuenta", + "serverError": "Algo salió mal. Por favor inténtalo de nuevo.", + "submit": "Iniciar sesión", + "submitting": "Iniciando sesión...", + "subtitle": "Orquestación de infraestructura distribuida", + "title": "Iniciar sesión en {company}", + "username": "Usuario" + }, + "register": { + "email": "Correo electrónico", + "emailTaken": "Correo electrónico ya registrado", + "hasAccount": "¿Ya tienes cuenta?", + "login": "Iniciar sesión", + "password": "Contraseña", + "serverError": "Algo salió mal. Por favor inténtalo de nuevo.", + "submit": "Crear cuenta", + "submitting": "Creando cuenta...", + "subtitle": "Empieza a orquestar tu infraestructura", + "title": "Crear tu cuenta", + "username": "Usuario", + "usernameTaken": "Nombre de usuario ya en uso", + "validation": { + "emailInvalid": "Introduce una dirección de correo válida", + "passwordLowercase": "Debe contener al menos una letra minúscula", + "passwordMax": "La contraseña no puede superar 30 caracteres", + "passwordMin": "La contraseña debe tener al menos 12 caracteres", + "passwordNumber": "Debe contener al menos un número", + "passwordSpecial": "Debe contener al menos un carácter especial", + "passwordUppercase": "Debe contener al menos una letra mayúscula", + "usernameChars": "Solo se permiten letras minúsculas, números, - y _", + "usernameEdge": "No puede comenzar ni terminar con - o _", + "usernameMax": "El usuario no puede superar 32 caracteres", + "usernameMin": "El usuario debe tener al menos 3 caracteres", + "usernameReserved": "Este nombre de usuario está reservado" + } + } + }, + "branding": { + "author": "Jaroc", + "madeWith": "Hecho con amor por" + } } diff --git a/lynx/dashboard/ui/next.config.ts b/lynx/dashboard/ui/next.config.ts index 06a5e74..36c691d 100644 --- a/lynx/dashboard/ui/next.config.ts +++ b/lynx/dashboard/ui/next.config.ts @@ -3,19 +3,9 @@ import createNextIntlPlugin from "next-intl/plugin"; const withNextIntl = createNextIntlPlugin("./src/i18n/request.ts"); -const backendUrl = process.env.BACKEND_URL ?? "http://localhost:8080"; - const nextConfig: NextConfig = { - reactCompiler: true, output: "standalone", - async rewrites() { - return [ - { - source: "/api/:path*", - destination: `${backendUrl}/:path*`, - }, - ]; - }, + reactCompiler: true, }; export default withNextIntl(nextConfig); diff --git a/lynx/dashboard/ui/package.json b/lynx/dashboard/ui/package.json index 37a7a50..85643a9 100644 --- a/lynx/dashboard/ui/package.json +++ b/lynx/dashboard/ui/package.json @@ -1,66 +1,63 @@ { - "name": "lynx-dashboard-ui", - "version": "0.1.0", - "private": true, - "scripts": { - "dev": "next dev", - "build": "next build", - "start": "next start", - "check": "biome check", - "check:fix": "biome check --write", - "typecheck": "tsc --noEmit", - "audit": "bun audit", - "test": "vitest run", - "test:watch": "vitest", - "test:e2e": "playwright test", - "test:e2e:ui": "playwright test --ui" - }, "dependencies": { - "@base-ui/react": "^1.4.1", + "@base-ui/react": "1.4.1", "@hookform/resolvers": "5.2.2", - "@tanstack/react-table": "^8.21.3", - "class-variance-authority": "^0.7.1", - "clsx": "^2.1.1", - "date-fns": "^4.1.0", - "embla-carousel-react": "^8.6.0", - "input-otp": "^1.4.2", - "lucide-react": "^1.14.0", - "next": "^16.2.6", + "@tanstack/react-table": "8.21.3", + "class-variance-authority": "0.7.1", + "clsx": "2.1.1", + "date-fns": "4.1.0", + "embla-carousel-react": "8.6.0", + "input-otp": "1.4.2", + "lucide-react": "1.16.0", + "next": "16.2.6", "next-intl": "4.12.0", - "next-themes": "^0.4.6", - "radix-ui": "^1.4.3", - "react": "^19.2.6", - "react-day-picker": "^10.0.0", - "react-dom": "^19.2.6", + "next-themes": "0.4.6", + "radix-ui": "1.4.3", + "react": "19.2.6", + "react-day-picker": "10.0.1", + "react-dom": "19.2.6", "react-hook-form": "7.75.0", - "react-resizable-panels": "^4.11.0", - "recharts": "^3.8.1", - "shadcn": "^4.7.0", - "sonner": "^2.0.7", - "tailwind-merge": "^3.6.0", - "tw-animate-css": "^1.4.0", - "vaul": "^1.1.2", + "react-resizable-panels": "4.11.1", + "recharts": "3.8.1", + "shadcn": "4.7.0", + "sonner": "2.0.7", + "tailwind-merge": "3.6.0", + "tw-animate-css": "1.4.0", + "vaul": "1.1.2", "zod": "4.4.3" }, "devDependencies": { - "@biomejs/biome": "^2.4.15", - "@playwright/test": "^1.52.0", - "@tailwindcss/postcss": "^4.3.0", + "@biomejs/biome": "2.4.15", + "@playwright/test": "1.60.0", + "@tailwindcss/postcss": "4.3.0", "@testing-library/jest-dom": "6.9.1", "@testing-library/react": "16.3.2", "@testing-library/user-event": "14.6.1", - "@types/node": "^24.12.4", - "@types/react": "^19.2.14", - "@types/react-dom": "^19.2.3", + "@types/node": "24.12.4", + "@types/react": "19.2.14", + "@types/react-dom": "19.2.3", "@vitejs/plugin-react": "6.0.2", - "babel-plugin-react-compiler": "^1.0.0", + "babel-plugin-react-compiler": "1.0.0", "jsdom": "29.1.1", - "tailwindcss": "^4.3.0", - "typescript": "^6.0.3", + "tailwindcss": "4.3.0", + "typescript": "6.0.3", "vitest": "4.1.6" }, + "name": "lynx-dashboard-ui", + "private": true, + "scripts": { + "audit": "bun audit", + "build": "next build", + "check": "biome check", + "check:fix": "biome check --write", + "start": "next start", + "test": "vitest run", + "test:e2e": "playwright test", + "typecheck": "tsc --noEmit" + }, "trustedDependencies": [ "sharp", "unrs-resolver" - ] + ], + "version": "0.1.0" } diff --git a/lynx/dashboard/ui/playwright.config.ts b/lynx/dashboard/ui/playwright.config.ts index 339c77a..b392b61 100644 --- a/lynx/dashboard/ui/playwright.config.ts +++ b/lynx/dashboard/ui/playwright.config.ts @@ -1,35 +1,26 @@ import { defineConfig, devices } from "@playwright/test"; export default defineConfig({ - testDir: "./src/e2e", - fullyParallel: true, forbidOnly: !!process.env.CI, - retries: process.env.CI ? 2 : 0, - workers: process.env.CI ? 1 : undefined, + fullyParallel: true, + + projects: [{ name: "chromium", use: { ...devices["Desktop Chrome"] } }], reporter: process.env.CI ? "github" : "list", + retries: process.env.CI ? 2 : 0, + testDir: "./src/e2e", use: { baseURL: process.env.PLAYWRIGHT_BASE_URL ?? "http://localhost:3000", trace: "on-first-retry", }, - projects: [ - { name: "chromium", use: { ...devices["Desktop Chrome"] } }, - ], - - webServer: process.env.CI - ? { + webServer: process.env.PLAYWRIGHT_BASE_URL + ? undefined + : { command: "bun run start", - url: "http://localhost:3000", - reuseExistingServer: false, + reuseExistingServer: !process.env.CI, timeout: 120_000, - } - : process.env.PLAYWRIGHT_NO_SERVER - ? undefined - : { - command: "bun run dev", url: "http://localhost:3000", - reuseExistingServer: true, - timeout: 120_000, - }, + }, + workers: process.env.CI ? 1 : undefined, }); diff --git a/lynx/dashboard/ui/src/__tests__/(auth)/schemas.test.ts b/lynx/dashboard/ui/src/__tests__/(auth)/schemas.test.ts index f3ac87b..9f597e2 100644 --- a/lynx/dashboard/ui/src/__tests__/(auth)/schemas.test.ts +++ b/lynx/dashboard/ui/src/__tests__/(auth)/schemas.test.ts @@ -8,15 +8,15 @@ import { registerSchema } from "@/schemas/(auth)/register"; describe("loginSchema", () => { it("accepts valid credentials", () => { - expect(loginSchema.safeParse({ username: "alice", password: "secret" }).success).toBe(true); + expect(loginSchema.safeParse({ password: "secret", username: "alice" }).success).toBe(true); }); it("rejects empty username", () => { - expect(loginSchema.safeParse({ username: "", password: "secret" }).success).toBe(false); + expect(loginSchema.safeParse({ password: "secret", username: "" }).success).toBe(false); }); it("rejects empty password", () => { - expect(loginSchema.safeParse({ username: "alice", password: "" }).success).toBe(false); + expect(loginSchema.safeParse({ password: "", username: "alice" }).success).toBe(false); }); it("rejects missing fields", () => { @@ -30,9 +30,9 @@ describe("loginSchema", () => { describe("registerSchema", () => { const valid = { - username: "alice42", email: "alice@example.com", password: "ValidP@ss12!", + username: "alice42", }; it("accepts valid registration data", () => { diff --git a/lynx/dashboard/ui/src/__tests__/(dashboard)/app/organizations/schemas.test.ts b/lynx/dashboard/ui/src/__tests__/(dashboard)/app/organizations/schemas.test.ts index 9159234..d765582 100644 --- a/lynx/dashboard/ui/src/__tests__/(dashboard)/app/organizations/schemas.test.ts +++ b/lynx/dashboard/ui/src/__tests__/(dashboard)/app/organizations/schemas.test.ts @@ -1,10 +1,7 @@ import { describe, expect, it } from "vitest"; import { registerAgentSchema } from "@/schemas/(dashboard)/app/agents"; import { createOrgSchema } from "@/schemas/(dashboard)/app/organizations"; -import { - createProjectSchema, - inviteMemberSchema, -} from "@/schemas/(dashboard)/app/organizations/[id]"; +import { createProjectSchema, inviteMemberSchema } from "@/schemas/(dashboard)/app/organizations/[id]"; import { addTunnelSchema, deployContainerSchema, @@ -87,25 +84,23 @@ describe("createOrgSchema", () => { describe("inviteMemberSchema", () => { it("accepts valid viewer invite", () => { - expect(inviteMemberSchema.safeParse({ username: "alice", role: "viewer" }).success).toBe(true); + expect(inviteMemberSchema.safeParse({ role: "viewer", username: "alice" }).success).toBe(true); }); it("accepts valid member invite", () => { - expect(inviteMemberSchema.safeParse({ username: "bob", role: "member" }).success).toBe(true); + expect(inviteMemberSchema.safeParse({ role: "member", username: "bob" }).success).toBe(true); }); it("accepts valid admin invite", () => { - expect(inviteMemberSchema.safeParse({ username: "carol", role: "admin" }).success).toBe(true); + expect(inviteMemberSchema.safeParse({ role: "admin", username: "carol" }).success).toBe(true); }); it("rejects unknown role", () => { - expect( - inviteMemberSchema.safeParse({ username: "dave", role: "superadmin" }).success, - ).toBe(false); + expect(inviteMemberSchema.safeParse({ role: "superadmin", username: "dave" }).success).toBe(false); }); it("rejects empty username", () => { - expect(inviteMemberSchema.safeParse({ username: "", role: "viewer" }).success).toBe(false); + expect(inviteMemberSchema.safeParse({ role: "viewer", username: "" }).success).toBe(false); }); it("rejects missing role", () => { @@ -118,7 +113,7 @@ describe("inviteMemberSchema", () => { // --------------------------------------------------------------------------- describe("createProjectSchema", () => { - const valid = { name: "Web App", slug: "web-app", agent_id: "some-uuid" }; + const valid = { agent_id: "some-uuid", name: "Web App", slug: "web-app" }; it("accepts valid project data", () => { expect(createProjectSchema.safeParse(valid).success).toBe(true); @@ -154,7 +149,7 @@ describe("createProjectSchema", () => { // --------------------------------------------------------------------------- describe("deployContainerSchema", () => { - const valid = { name: "nginx", image: "docker.io/library/nginx:latest" }; + const valid = { image: "docker.io/library/nginx:latest", name: "nginx" }; it("accepts minimal valid container data (no optional fields)", () => { expect(deployContainerSchema.safeParse(valid).success).toBe(true); @@ -164,10 +159,10 @@ describe("deployContainerSchema", () => { expect( deployContainerSchema.safeParse({ ...valid, - ports: "80:80", - env: "FOO=bar", cpus: 0.5, + env: "FOO=bar", memory_mb: 256, + ports: "80:80", }).success, ).toBe(true); }); @@ -235,9 +230,9 @@ describe("resourceFormSchema", () => { describe("addTunnelSchema", () => { const valid = { - target_agent_id: "agent-uuid-xyz", image: "docker.io/library/nginx:latest", replica_count: 2, + target_agent_id: "agent-uuid-xyz", }; it("accepts valid tunnel data", () => { diff --git a/lynx/dashboard/ui/src/__tests__/(dashboard)/app/settings/schemas.test.ts b/lynx/dashboard/ui/src/__tests__/(dashboard)/app/settings/schemas.test.ts index e35ace7..179aea7 100644 --- a/lynx/dashboard/ui/src/__tests__/(dashboard)/app/settings/schemas.test.ts +++ b/lynx/dashboard/ui/src/__tests__/(dashboard)/app/settings/schemas.test.ts @@ -90,44 +90,34 @@ describe("certUploadSchema", () => { const validKey = "-----BEGIN PRIVATE KEY-----\nMIIByyy\n-----END PRIVATE KEY-----"; it("accepts cloudflare cert with valid PEM", () => { - expect( - certUploadSchema.safeParse({ cert_type: "cloudflare", cert_pem: validPem }).success, - ).toBe(true); + expect(certUploadSchema.safeParse({ cert_pem: validPem, cert_type: "cloudflare" }).success).toBe(true); }); it("accepts custom cert with cert + key", () => { expect( certUploadSchema.safeParse({ - cert_type: "custom", cert_pem: validPem, + cert_type: "custom", key_pem: validKey, }).success, ).toBe(true); }); it("rejects cert without PEM header", () => { - expect( - certUploadSchema.safeParse({ cert_type: "cloudflare", cert_pem: "not a cert" }).success, - ).toBe(false); + expect(certUploadSchema.safeParse({ cert_pem: "not a cert", cert_type: "cloudflare" }).success).toBe(false); }); it("rejects empty cert_pem", () => { - expect( - certUploadSchema.safeParse({ cert_type: "cloudflare", cert_pem: "" }).success, - ).toBe(false); + expect(certUploadSchema.safeParse({ cert_pem: "", cert_type: "cloudflare" }).success).toBe(false); }); it("rejects cert exceeding 64 KB", () => { const big = "-----BEGIN CERTIFICATE-----\n" + "A".repeat(65 * 1024); - expect( - certUploadSchema.safeParse({ cert_type: "cloudflare", cert_pem: big }).success, - ).toBe(false); + expect(certUploadSchema.safeParse({ cert_pem: big, cert_type: "cloudflare" }).success).toBe(false); }); it("rejects unknown cert_type", () => { - expect( - certUploadSchema.safeParse({ cert_type: "letsencrypt", cert_pem: validPem }).success, - ).toBe(false); + expect(certUploadSchema.safeParse({ cert_pem: validPem, cert_type: "letsencrypt" }).success).toBe(false); }); }); @@ -136,16 +126,14 @@ describe("certUploadSchema", () => { // --------------------------------------------------------------------------- describe("migrationStartSchema", () => { - const valid = { target_url: "https://10.0.0.2:19443", migration_token: "tok-abc123" }; + const valid = { migration_token: "tok-abc123", target_url: "https://10.0.0.2:19443" }; it("accepts valid migration start data", () => { expect(migrationStartSchema.safeParse(valid).success).toBe(true); }); it("rejects non-URL target_url", () => { - expect(migrationStartSchema.safeParse({ ...valid, target_url: "not-a-url" }).success).toBe( - false, - ); + expect(migrationStartSchema.safeParse({ ...valid, target_url: "not-a-url" }).success).toBe(false); }); it("rejects empty target_url", () => { diff --git a/lynx/dashboard/ui/src/actions/(auth)/login/index.ts b/lynx/dashboard/ui/src/actions/(auth)/login/index.ts index 6b60b9c..d0c7da9 100644 --- a/lynx/dashboard/ui/src/actions/(auth)/login/index.ts +++ b/lynx/dashboard/ui/src/actions/(auth)/login/index.ts @@ -1,17 +1,14 @@ "use server"; -import { apiFetch } from "@/lib/api"; import { cookies } from "next/headers"; +import { apiFetch } from "@/lib/api"; import type { LoginInput } from "@/schemas/(auth)/login"; type LoginResult = | { success: true; forcePasswordChange?: boolean } | { success: false; error: string; retryAfter?: number }; -export async function loginAction( - locale: string, - data: LoginInput, -): Promise { +export async function loginAction(locale: string, data: LoginInput): Promise { const result = await apiFetch<{ access_token: string; refresh_token: string; @@ -19,19 +16,19 @@ export async function loginAction( force_password_change: boolean; theme: string; }>("/auth/login", { + body: JSON.stringify({ password: data.password, username: data.username }), method: "POST", - body: JSON.stringify({ username: data.username, password: data.password }), }); if (!result.ok) { if (result.error === "invalid_credentials") { - return { success: false, error: "invalidCredentials" }; + return { error: "invalidCredentials", success: false }; } if (result.error === "rate_limited") { const minutes = result.retryAfter ? Math.ceil(result.retryAfter / 60) : 15; - return { success: false, error: "rateLimited", retryAfter: minutes }; + return { error: "rateLimited", retryAfter: minutes, success: false }; } - return { success: false, error: "serverError" }; + return { error: "serverError", success: false }; } const jar = await cookies(); @@ -39,25 +36,25 @@ export async function loginAction( jar.set("access_token", result.data.access_token, { httpOnly: true, - secure, - sameSite: "strict", maxAge: result.data.expires_in, path: "/", + sameSite: "strict", + secure, }); jar.set("refresh_token", result.data.refresh_token, { httpOnly: true, - secure, - sameSite: "strict", maxAge: 86400, path: "/", + sameSite: "strict", + secure, }); jar.set("theme_preference", result.data.theme ?? "system", { httpOnly: false, - secure, - sameSite: "strict", maxAge: 60 * 60 * 24 * 365, path: "/", + sameSite: "strict", + secure, }); - return { success: true, forcePasswordChange: result.data.force_password_change }; + return { forcePasswordChange: result.data.force_password_change, success: true }; } diff --git a/lynx/dashboard/ui/src/actions/(auth)/register/index.ts b/lynx/dashboard/ui/src/actions/(auth)/register/index.ts index ce9e311..881624e 100644 --- a/lynx/dashboard/ui/src/actions/(auth)/register/index.ts +++ b/lynx/dashboard/ui/src/actions/(auth)/register/index.ts @@ -3,28 +3,23 @@ import { apiFetch } from "@/lib/api"; import type { RegisterInput } from "@/schemas/(auth)/register"; -type RegisterResult = - | { success: true } - | { success: false; error: string }; +type RegisterResult = { success: true } | { success: false; error: string }; -export async function registerAction( - _locale: string, - data: RegisterInput, -): Promise { +export async function registerAction(_locale: string, data: RegisterInput): Promise { const result = await apiFetch("/auth/register", { - method: "POST", body: JSON.stringify({ - username: data.username, email: data.email, password: data.password, + username: data.username, }), + method: "POST", }); if (!result.ok) { if (result.error === "conflict") { - return { success: false, error: "usernameTaken" }; + return { error: "usernameTaken", success: false }; } - return { success: false, error: "serverError" }; + return { error: "serverError", success: false }; } return { success: true }; diff --git a/lynx/dashboard/ui/src/actions/(dashboard)/app/admin/users.ts b/lynx/dashboard/ui/src/actions/(dashboard)/app/admin/users.ts index 238b74b..dd60b75 100644 --- a/lynx/dashboard/ui/src/actions/(dashboard)/app/admin/users.ts +++ b/lynx/dashboard/ui/src/actions/(dashboard)/app/admin/users.ts @@ -1,7 +1,7 @@ "use server"; -import { apiFetch } from "@/lib/api"; import { cookies } from "next/headers"; +import { apiFetch } from "@/lib/api"; async function token(): Promise { const jar = await cookies(); @@ -38,17 +38,17 @@ export async function listUsersAction(): Promise { export async function deleteUserAction(userId: string): Promise<{ success: boolean; error?: string }> { const tok = await token(); const res = await apiFetch(`/admin/users/${userId}`, { - method: "DELETE", headers: { Authorization: `Bearer ${tok}` }, + method: "DELETE", }); - return res.ok ? { success: true } : { success: false, error: (res as { ok: false; error: string }).error }; + return res.ok ? { success: true } : { error: (res as { ok: false; error: string }).error, success: false }; } export async function forcePasswordChangeAction(userId: string): Promise<{ success: boolean }> { const tok = await token(); const res = await apiFetch(`/admin/users/${userId}/force-password-change`, { - method: "POST", headers: { Authorization: `Bearer ${tok}` }, + method: "POST", }); return { success: res.ok }; } @@ -56,8 +56,8 @@ export async function forcePasswordChangeAction(userId: string): Promise<{ succe export async function addUserRoleAction(userId: string, roleId: string): Promise<{ success: boolean }> { const tok = await token(); const res = await apiFetch(`/admin/users/${userId}/roles/${roleId}`, { - method: "POST", headers: { Authorization: `Bearer ${tok}` }, + method: "POST", }); return { success: res.ok }; } @@ -65,8 +65,8 @@ export async function addUserRoleAction(userId: string, roleId: string): Promise export async function removeUserRoleAction(userId: string, roleId: string): Promise<{ success: boolean }> { const tok = await token(); const res = await apiFetch(`/admin/users/${userId}/roles/${roleId}`, { - method: "DELETE", headers: { Authorization: `Bearer ${tok}` }, + method: "DELETE", }); return { success: res.ok }; } @@ -92,37 +92,40 @@ export async function listPermissionsAction(): Promise { export async function createRoleAction(name: string): Promise<{ success: boolean; id?: string; error?: string }> { const tok = await token(); const res = await apiFetch<{ id: string; name: string }>("/admin/roles", { - method: "POST", - headers: { Authorization: `Bearer ${tok}` }, body: JSON.stringify({ name }), + headers: { Authorization: `Bearer ${tok}` }, + method: "POST", }); - if (res.ok) return { success: true, id: res.data.id }; - return { success: false, error: (res as { ok: false; error: string }).error }; + if (res.ok) return { id: res.data.id, success: true }; + return { error: (res as { ok: false; error: string }).error, success: false }; } export async function deleteRoleAction(roleId: string): Promise<{ success: boolean; error?: string }> { const tok = await token(); const res = await apiFetch(`/admin/roles/${roleId}`, { - method: "DELETE", headers: { Authorization: `Bearer ${tok}` }, + method: "DELETE", }); - return res.ok ? { success: true } : { success: false, error: (res as { ok: false; error: string }).error }; + return res.ok ? { success: true } : { error: (res as { ok: false; error: string }).error, success: false }; } export async function addRolePermissionAction(roleId: string, permId: string): Promise<{ success: boolean }> { const tok = await token(); const res = await apiFetch(`/admin/roles/${roleId}/permissions/${permId}`, { - method: "POST", headers: { Authorization: `Bearer ${tok}` }, + method: "POST", }); return { success: res.ok }; } -export async function removeRolePermissionAction(roleId: string, permId: string): Promise<{ success: boolean; error?: string }> { +export async function removeRolePermissionAction( + roleId: string, + permId: string, +): Promise<{ success: boolean; error?: string }> { const tok = await token(); const res = await apiFetch(`/admin/roles/${roleId}/permissions/${permId}`, { - method: "DELETE", headers: { Authorization: `Bearer ${tok}` }, + method: "DELETE", }); - return res.ok ? { success: true } : { success: false, error: (res as { ok: false; error: string }).error }; + return res.ok ? { success: true } : { error: (res as { ok: false; error: string }).error, success: false }; } diff --git a/lynx/dashboard/ui/src/actions/(dashboard)/app/logout.ts b/lynx/dashboard/ui/src/actions/(dashboard)/app/logout.ts index 60132a2..65f7cde 100644 --- a/lynx/dashboard/ui/src/actions/(dashboard)/app/logout.ts +++ b/lynx/dashboard/ui/src/actions/(dashboard)/app/logout.ts @@ -1,8 +1,8 @@ "use server"; -import { apiFetch } from "@/lib/api"; import { cookies } from "next/headers"; import { redirect } from "next/navigation"; +import { apiFetch } from "@/lib/api"; export async function logoutAction(locale: string) { const jar = await cookies(); @@ -10,8 +10,8 @@ export async function logoutAction(locale: string) { if (accessToken) { await apiFetch("/auth/logout", { - method: "POST", headers: { Authorization: `Bearer ${accessToken}` }, + method: "POST", }); } diff --git a/lynx/dashboard/ui/src/actions/(dashboard)/app/settings/migration.ts b/lynx/dashboard/ui/src/actions/(dashboard)/app/settings/migration.ts index 34e5e52..26dc6c0 100644 --- a/lynx/dashboard/ui/src/actions/(dashboard)/app/settings/migration.ts +++ b/lynx/dashboard/ui/src/actions/(dashboard)/app/settings/migration.ts @@ -20,8 +20,8 @@ export async function getMigrationStatus(): Promise<{ const tok = await authToken(); try { const res = await fetch(`${BACKEND_URL}/migration`, { - headers: { Authorization: `Bearer ${tok}` }, cache: "no-store", + headers: { Authorization: `Bearer ${tok}` }, }); if (!res.ok) return null; return res.json(); @@ -38,14 +38,14 @@ export async function prepareMigration(): Promise<{ const tok = await authToken(); try { const res = await fetch(`${BACKEND_URL}/migration/prepare`, { - method: "POST", headers: { Authorization: `Bearer ${tok}` }, + method: "POST", }); - if (!res.ok) return { ok: false, error: `${res.status}` }; + if (!res.ok) return { error: `${res.status}`, ok: false }; const data = (await res.json()) as { migration_token: string }; - return { ok: true, migration_token: data.migration_token }; + return { migration_token: data.migration_token, ok: true }; } catch { - return { ok: false, error: "network_error" }; + return { error: "network_error", ok: false }; } } @@ -56,17 +56,17 @@ export async function startMigration( const tok = await authToken(); try { const res = await fetch(`${BACKEND_URL}/migration/start`, { - method: "POST", + body: JSON.stringify({ migration_token: migrationToken, target_url: targetUrl }), headers: { - "Content-Type": "application/json", Authorization: `Bearer ${tok}`, + "Content-Type": "application/json", }, - body: JSON.stringify({ target_url: targetUrl, migration_token: migrationToken }), + method: "POST", }); - if (!res.ok) return { ok: false, error: `${res.status}` }; + if (!res.ok) return { error: `${res.status}`, ok: false }; return { ok: true }; } catch { - return { ok: false, error: "network_error" }; + return { error: "network_error", ok: false }; } } @@ -74,13 +74,13 @@ export async function abortMigration(): Promise<{ ok: boolean; error?: string }> const tok = await authToken(); try { const res = await fetch(`${BACKEND_URL}/migration/abort`, { - method: "POST", headers: { Authorization: `Bearer ${tok}` }, + method: "POST", }); - if (!res.ok) return { ok: false, error: `${res.status}` }; + if (!res.ok) return { error: `${res.status}`, ok: false }; return { ok: true }; } catch { - return { ok: false, error: "network_error" }; + return { error: "network_error", ok: false }; } } @@ -91,12 +91,12 @@ export async function confirmMigrationShutdown(): Promise<{ const tok = await authToken(); try { const res = await fetch(`${BACKEND_URL}/migration/confirm-shutdown`, { - method: "POST", headers: { Authorization: `Bearer ${tok}` }, + method: "POST", }); - if (!res.ok) return { ok: false, error: `${res.status}` }; + if (!res.ok) return { error: `${res.status}`, ok: false }; return { ok: true }; } catch { - return { ok: false, error: "network_error" }; + return { error: "network_error", ok: false }; } } diff --git a/lynx/dashboard/ui/src/actions/(dashboard)/app/settings/preferences.ts b/lynx/dashboard/ui/src/actions/(dashboard)/app/settings/preferences.ts index a8ced06..8951074 100644 --- a/lynx/dashboard/ui/src/actions/(dashboard)/app/settings/preferences.ts +++ b/lynx/dashboard/ui/src/actions/(dashboard)/app/settings/preferences.ts @@ -1,7 +1,7 @@ "use server"; -import { apiFetch } from "@/lib/api"; import { cookies } from "next/headers"; +import { apiFetch } from "@/lib/api"; async function token(): Promise { const jar = await cookies(); @@ -11,25 +11,25 @@ async function token(): Promise { export async function updateThemeAction(theme: string): Promise { const tok = await token(); await apiFetch("/auth/me/preferences", { - method: "POST", - headers: { Authorization: `Bearer ${tok}` }, body: JSON.stringify({ theme }), + headers: { Authorization: `Bearer ${tok}` }, + method: "POST", }); const jar = await cookies(); jar.set("theme_preference", theme, { - path: "/", httpOnly: false, + maxAge: 60 * 60 * 24 * 365, + path: "/", sameSite: "strict", secure: process.env.NODE_ENV === "production", - maxAge: 60 * 60 * 24 * 365, }); } export async function updateLocaleAction(locale: string): Promise { const tok = await token(); await apiFetch("/auth/me/preferences", { - method: "POST", - headers: { Authorization: `Bearer ${tok}` }, body: JSON.stringify({ locale }), + headers: { Authorization: `Bearer ${tok}` }, + method: "POST", }); } diff --git a/lynx/dashboard/ui/src/actions/(dashboard)/app/settings/profile.ts b/lynx/dashboard/ui/src/actions/(dashboard)/app/settings/profile.ts index 13df369..3457618 100644 --- a/lynx/dashboard/ui/src/actions/(dashboard)/app/settings/profile.ts +++ b/lynx/dashboard/ui/src/actions/(dashboard)/app/settings/profile.ts @@ -12,15 +12,15 @@ export async function changePassword( try { const res = await fetch(`${BACKEND_URL}/auth/change-password`, { - method: "POST", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${tok}`, - }, body: JSON.stringify({ current_password: currentPassword, new_password: newPassword, }), + headers: { + Authorization: `Bearer ${tok}`, + "Content-Type": "application/json", + }, + method: "POST", }); if (res.ok) { @@ -46,8 +46,8 @@ export async function getMe(): Promise<{ try { const res = await fetch(`${BACKEND_URL}/auth/me`, { - headers: { Authorization: `Bearer ${tok}` }, cache: "no-store", + headers: { Authorization: `Bearer ${tok}` }, }); if (!res.ok) return null; return res.json(); @@ -62,12 +62,12 @@ export async function toggleSingleSession(enabled: boolean): Promise<{ ok: boole try { const res = await fetch(`${BACKEND_URL}/auth/me/single-session`, { - method: "POST", + body: JSON.stringify({ enabled }), headers: { - "Content-Type": "application/json", Authorization: `Bearer ${tok}`, + "Content-Type": "application/json", }, - body: JSON.stringify({ enabled }), + method: "POST", }); return { ok: res.ok }; } catch { diff --git a/lynx/dashboard/ui/src/app/[locale]/(auth)/login/page.tsx b/lynx/dashboard/ui/src/app/[locale]/(auth)/login/page.tsx index 9210533..b2b93df 100644 --- a/lynx/dashboard/ui/src/app/[locale]/(auth)/login/page.tsx +++ b/lynx/dashboard/ui/src/app/[locale]/(auth)/login/page.tsx @@ -1,7 +1,6 @@ - import { getTranslations } from "next-intl/server"; -import { BACKEND_URL } from "@/lib/api"; import { LoginForm } from "@/components/(auth)/login/LoginForm"; +import { BACKEND_URL } from "@/lib/api"; async function fetchCompanyName(): Promise { try { @@ -16,7 +15,7 @@ async function fetchCompanyName(): Promise { } } -export default async function LoginPage({ params }: { params: Promise<{ locale: string }>; }) { +export default async function LoginPage({ params }: { params: Promise<{ locale: string }> }) { const { locale } = await params; const [t, companyName] = await Promise.all([ getTranslations({ locale, namespace: "auth.login" }), @@ -27,9 +26,7 @@ export default async function LoginPage({ params }: { params: Promise<{ locale:
-

- {t("title", { company: companyName })} -

+

{t("title", { company: companyName })}

{t("subtitle")}

@@ -47,19 +44,19 @@ async function Branding({ locale }: { locale: string }) {
{t("madeWith")}{" "} {t("author")} {" · "} lynx diff --git a/lynx/dashboard/ui/src/app/[locale]/(auth)/register/page.tsx b/lynx/dashboard/ui/src/app/[locale]/(auth)/register/page.tsx index bbc8387..3300e21 100644 --- a/lynx/dashboard/ui/src/app/[locale]/(auth)/register/page.tsx +++ b/lynx/dashboard/ui/src/app/[locale]/(auth)/register/page.tsx @@ -1,10 +1,7 @@ - import { getTranslations } from "next-intl/server"; import { RegisterForm } from "@/components/(auth)/register/RegisterForm"; -export default async function RegisterPage({ - params, -}: { params: Promise<{ locale: string }>; }) { +export default async function RegisterPage({ params }: { params: Promise<{ locale: string }> }) { const { locale } = await params; const t = await getTranslations({ locale, namespace: "auth.register" }); @@ -30,19 +27,19 @@ async function Branding({ locale }: { locale: string }) {
{t("madeWith")}{" "} {t("author")} {" · "} lynx diff --git a/lynx/dashboard/ui/src/app/[locale]/(dashboard)/app/admin/page.tsx b/lynx/dashboard/ui/src/app/[locale]/(dashboard)/app/admin/page.tsx index 4824aa5..b5797fe 100644 --- a/lynx/dashboard/ui/src/app/[locale]/(dashboard)/app/admin/page.tsx +++ b/lynx/dashboard/ui/src/app/[locale]/(dashboard)/app/admin/page.tsx @@ -1,19 +1,19 @@ -import { Suspense } from "react"; -import { getTranslations } from "next-intl/server"; -import { redirect } from "next/navigation"; import { cookies } from "next/headers"; -import { BACKEND_URL } from "@/lib/api"; +import { redirect } from "next/navigation"; +import { getTranslations } from "next-intl/server"; +import { Suspense } from "react"; import { - listUsersAction, - listRolesAction, listPermissionsAction, - type UserRow, - type RoleRow, + listRolesAction, + listUsersAction, type PermRef, + type RoleRow, + type UserRow, } from "@/actions/(dashboard)/app/admin/users"; -import { UsersPanel } from "@/components/(dashboard)/app/admin/UsersPanel"; import { RolesPanel } from "@/components/(dashboard)/app/admin/RolesPanel"; +import { UsersPanel } from "@/components/(dashboard)/app/admin/UsersPanel"; import { Skeleton } from "@/components/ui/skeleton"; +import { BACKEND_URL } from "@/lib/api"; // --------------------------------------------------------------------------- // Guard: redirect non-admins @@ -22,8 +22,8 @@ import { Skeleton } from "@/components/ui/skeleton"; async function assertAdmin(token: string, locale: string) { try { const res = await fetch(`${BACKEND_URL}/auth/me`, { - headers: { Authorization: `Bearer ${token}` }, cache: "no-store", + headers: { Authorization: `Bearer ${token}` }, }); if (!res.ok) redirect(`/${locale}/app`); const data = (await res.json()) as { is_admin?: boolean }; @@ -47,55 +47,51 @@ async function AdminData({ locale }: { locale: string }) { ]); const userLabels = { - deleteUser: t("deleteUser"), + addRole: t("addRole"), + addRoleError: t("addRoleError"), + addRoleSuccess: t("addRoleSuccess"), deleteConfirm: t("deleteConfirm"), - deleteSuccess: t("deleteSuccess"), deleteError: t("deleteError"), + deleteSuccess: t("deleteSuccess"), + deleteUser: t("deleteUser"), forcePasswordChange: t("forcePasswordChange"), - forcePasswordChangeSuccess: t("forcePasswordChangeSuccess"), forcePasswordChangeError: t("forcePasswordChangeError"), - addRole: t("addRole"), - addRoleSuccess: t("addRoleSuccess"), - addRoleError: t("addRoleError"), + forcePasswordChangeSuccess: t("forcePasswordChangeSuccess"), + noRoles: t("noRoles"), removeRole: t("removeRole"), - removeRoleSuccess: t("removeRoleSuccess"), removeRoleError: t("removeRoleError"), - noRoles: t("noRoles"), + removeRoleSuccess: t("removeRoleSuccess"), selectRole: t("selectRole"), }; const roleLabels = { + addPermission: t("addPermission"), + addPermissionError: t("addPermissionError"), + addPermissionSuccess: t("addPermissionSuccess"), createRole: t("createRole"), - createRoleSuccess: t("createRoleSuccess"), createRoleError: t("createRoleError"), + createRoleSuccess: t("createRoleSuccess"), deleteRole: t("deleteRole"), deleteRoleConfirm: t("deleteRoleConfirm"), - deleteRoleSuccess: t("deleteRoleSuccess"), deleteRoleError: t("deleteRoleError"), - addPermission: t("addPermission"), - addPermissionSuccess: t("addPermissionSuccess"), - addPermissionError: t("addPermissionError"), + deleteRoleSuccess: t("deleteRoleSuccess"), + noPermissions: t("noPermissions"), removePermission: t("removePermission"), - removePermissionSuccess: t("removePermissionSuccess"), removePermissionError: t("removePermissionError"), + removePermissionSuccess: t("removePermissionSuccess"), roleName: t("roleName"), - noPermissions: t("noPermissions"), }; return ( <>
-

- {t("users")} -

- +

{t("users")}

+
-

- {t("roles")} -

- +

{t("roles")}

+
); @@ -138,9 +134,7 @@ function AdminSkeleton() { // Page // --------------------------------------------------------------------------- -export default async function AdminPage({ - params, -}: { params: Promise<{ locale: string }> }) { +export default async function AdminPage({ params }: { params: Promise<{ locale: string }> }) { const { locale } = await params; const t = await getTranslations({ locale, namespace: "admin" }); diff --git a/lynx/dashboard/ui/src/app/[locale]/(dashboard)/app/agents/[id]/audit-log/page.tsx b/lynx/dashboard/ui/src/app/[locale]/(dashboard)/app/agents/[id]/audit-log/page.tsx index c8dbdd5..e0e466b 100644 --- a/lynx/dashboard/ui/src/app/[locale]/(dashboard)/app/agents/[id]/audit-log/page.tsx +++ b/lynx/dashboard/ui/src/app/[locale]/(dashboard)/app/agents/[id]/audit-log/page.tsx @@ -1,68 +1,60 @@ +import { ChevronRight } from "lucide-react"; import { cookies } from "next/headers"; +import Link from "next/link"; import { notFound } from "next/navigation"; import { getTranslations } from "next-intl/server"; -import { BACKEND_URL } from "@/lib/api"; -import Link from "next/link"; -import { ChevronRight } from "lucide-react"; import { Badge } from "@/components/ui/badge"; +import { BACKEND_URL } from "@/lib/api"; interface AuditEntry { - id: string; agent_id: string; - organization_id: string | null; - user_id: string | null; command_type: string; - result: "success" | "rejected" | "failed"; - error: string | null; - entry_hash: string; created_at: string; + entry_hash: string; + error: string | null; + id: string; + organization_id: string | null; + result: "success" | "rejected" | "failed"; + user_id: string | null; } interface AuditResponse { entries: AuditEntry[]; - total: number; limit: number; offset: number; + total: number; } -async function fetchAuditLog( - token: string, - agentId: string, - limit = 50, - offset = 0, -): Promise { +async function fetchAuditLog(token: string, agentId: string, limit = 50, offset = 0): Promise { try { - const res = await fetch( - `${BACKEND_URL}/agents/${agentId}/audit-log?limit=${limit}&offset=${offset}`, - { headers: { Authorization: `Bearer ${token}` }, cache: "no-store" }, - ); + const res = await fetch(`${BACKEND_URL}/agents/${agentId}/audit-log?limit=${limit}&offset=${offset}`, { + cache: "no-store", + headers: { Authorization: `Bearer ${token}` }, + }); if (res.status === 404) return null; - if (!res.ok) return { entries: [], total: 0, limit, offset }; + if (!res.ok) return { entries: [], limit, offset, total: 0 }; return res.json(); } catch { - return { entries: [], total: 0, limit, offset }; + return { entries: [], limit, offset, total: 0 }; } } -const RESULT_VARIANT: Record< - AuditEntry["result"], - "default" | "destructive" | "secondary" -> = { - success: "default", - rejected: "secondary", +const RESULT_VARIANT: Record = { failed: "destructive", + rejected: "secondary", + success: "default", }; function formatTime(ts: string): string { const d = new Date(ts); return d.toLocaleString("en-GB", { - year: "numeric", - month: "short", day: "numeric", hour: "2-digit", + hour12: false, minute: "2-digit", + month: "short", second: "2-digit", - hour12: false, + year: "numeric", }); } @@ -94,10 +86,7 @@ export default async function AuditLogPage({ return (
); diff --git a/lynx/dashboard/ui/src/app/[locale]/(dashboard)/app/layout.tsx b/lynx/dashboard/ui/src/app/[locale]/(dashboard)/app/layout.tsx index 33d3dca..8ce2ce7 100644 --- a/lynx/dashboard/ui/src/app/[locale]/(dashboard)/app/layout.tsx +++ b/lynx/dashboard/ui/src/app/[locale]/(dashboard)/app/layout.tsx @@ -1,23 +1,22 @@ - import { cookies } from "next/headers"; import { redirect } from "next/navigation"; -import { BACKEND_URL } from "@/lib/api"; import { Sidebar } from "@/components/(dashboard)/app/Sidebar"; +import { BACKEND_URL } from "@/lib/api"; interface Branding { + accent_color: string; company_name: string; logo_url: string | null; primary_color: string; secondary_color: string; - accent_color: string; } const DEFAULTS: Branding = { + accent_color: "#6366f1", company_name: "Lynx", logo_url: null, primary_color: "#0f172a", secondary_color: "#38bdf8", - accent_color: "#6366f1", }; async function fetchBranding(): Promise { @@ -35,8 +34,8 @@ async function fetchBranding(): Promise { async function fetchIsAdmin(token: string): Promise { try { const res = await fetch(`${BACKEND_URL}/auth/me`, { - headers: { Authorization: `Bearer ${token}` }, cache: "no-store", + headers: { Authorization: `Bearer ${token}` }, }); if (!res.ok) return false; const data = (await res.json()) as { is_admin?: boolean }; @@ -49,7 +48,10 @@ async function fetchIsAdmin(token: string): Promise { export default async function AppLayout({ children, params, -}: { children: React.ReactNode; params: Promise<{ locale: string }>; }) { +}: { + children: React.ReactNode; + params: Promise<{ locale: string }>; +}) { const { locale } = await params; const jar = await cookies(); @@ -68,14 +70,12 @@ export default async function AppLayout({ return (
-
- {children} -
+
{children}
); } diff --git a/lynx/dashboard/ui/src/app/[locale]/(dashboard)/app/organizations/[id]/page.tsx b/lynx/dashboard/ui/src/app/[locale]/(dashboard)/app/organizations/[id]/page.tsx index 27d3190..a7a0860 100644 --- a/lynx/dashboard/ui/src/app/[locale]/(dashboard)/app/organizations/[id]/page.tsx +++ b/lynx/dashboard/ui/src/app/[locale]/(dashboard)/app/organizations/[id]/page.tsx @@ -1,34 +1,34 @@ import { cookies } from "next/headers"; +import Link from "next/link"; import { notFound } from "next/navigation"; import { getTranslations } from "next-intl/server"; -import { BACKEND_URL } from "@/lib/api"; -import { Badge } from "@/components/ui/badge"; -import Link from "next/link"; +import { CreateProjectDialog } from "@/components/(dashboard)/app/organizations/[id]/CreateProjectDialog"; import { InviteDialog } from "@/components/(dashboard)/app/organizations/[id]/InviteDialog"; import { RemoveMemberButton } from "@/components/(dashboard)/app/organizations/[id]/RemoveMemberButton"; -import { CreateProjectDialog } from "@/components/(dashboard)/app/organizations/[id]/CreateProjectDialog"; +import { Badge } from "@/components/ui/badge"; +import { BACKEND_URL } from "@/lib/api"; interface Org { + created_at: string; id: string; name: string; - slug: string; owner_id: string; - created_at: string; + slug: string; } interface Member { + joined_at: string; + role: string; user_id: string; username: string; - role: string; - joined_at: string; } interface Project { + agent_id: string; + created_at: string; id: string; name: string; slug: string; - agent_id: string; - created_at: string; } interface Agent { @@ -40,8 +40,8 @@ interface Agent { async function fetchOrg(token: string, id: string): Promise { try { const res = await fetch(`${BACKEND_URL}/organizations/${id}`, { - headers: { Authorization: `Bearer ${token}` }, cache: "no-store", + headers: { Authorization: `Bearer ${token}` }, }); if (!res.ok) return null; return res.json(); @@ -53,8 +53,8 @@ async function fetchOrg(token: string, id: string): Promise { async function fetchMembers(token: string, id: string): Promise { try { const res = await fetch(`${BACKEND_URL}/organizations/${id}/members`, { - headers: { Authorization: `Bearer ${token}` }, cache: "no-store", + headers: { Authorization: `Bearer ${token}` }, }); if (!res.ok) return []; return res.json(); @@ -66,8 +66,8 @@ async function fetchMembers(token: string, id: string): Promise { async function fetchAgents(token: string): Promise { try { const res = await fetch(`${BACKEND_URL}/agents`, { - headers: { Authorization: `Bearer ${token}` }, cache: "no-store", + headers: { Authorization: `Bearer ${token}` }, }); if (!res.ok) return []; return res.json(); @@ -79,8 +79,8 @@ async function fetchAgents(token: string): Promise { async function fetchProjects(token: string, id: string): Promise { try { const res = await fetch(`${BACKEND_URL}/organizations/${id}/projects`, { - headers: { Authorization: `Bearer ${token}` }, cache: "no-store", + headers: { Authorization: `Bearer ${token}` }, }); if (!res.ok) return []; return res.json(); @@ -90,22 +90,15 @@ async function fetchProjects(token: string, id: string): Promise { } const ROLE_VARIANT: Record = { - owner: "default", admin: "secondary", member: "outline", + owner: "default", viewer: "outline", }; -export default async function OrgDetailPage({ - params, -}: { - params: Promise<{ locale: string; id: string }>; -}) { +export default async function OrgDetailPage({ params }: { params: Promise<{ locale: string; id: string }> }) { const { locale, id } = await params; - const [t, jar] = await Promise.all([ - getTranslations({ locale, namespace: "app.organizations" }), - cookies(), - ]); + const [t, jar] = await Promise.all([getTranslations({ locale, namespace: "app.organizations" }), cookies()]); const tok = jar.get("access_token")?.value ?? ""; const [org, members, projects, agents] = await Promise.all([ @@ -117,14 +110,14 @@ export default async function OrgDetailPage({ if (!org) notFound(); - const currentUserId = members.find( - (m) => m.role === "owner" && org.owner_id === m.user_id, - )?.user_id; + const currentUserId = members.find((m) => m.role === "owner" && org.owner_id === m.user_id)?.user_id; return (
-

{t("slug")}{" "}{org.slug}

+

+ {t("slug")} {org.slug} +

{org.name}

@@ -134,48 +127,39 @@ export default async function OrgDetailPage({ {t("members")} ({members.length})
{members.map((m) => ( -
+
- - {m.username} - - - {m.role} - + {m.username} + {m.role}
{m.role !== "owner" && ( )}
))} {members.length === 0 && ( -

- {t("noMembers")} -

+

{t("noMembers")}

)}
@@ -186,20 +170,20 @@ export default async function OrgDetailPage({ {t("projects")} ({projects.length})
{projects.length === 0 ? ( @@ -208,15 +192,15 @@ export default async function OrgDetailPage({
{projects.map((p) => (

{p.name}

{p.slug}

- + {p.agent_id.slice(0, 8)} diff --git a/lynx/dashboard/ui/src/app/[locale]/(dashboard)/app/organizations/[id]/projects/[proj_id]/page.tsx b/lynx/dashboard/ui/src/app/[locale]/(dashboard)/app/organizations/[id]/projects/[proj_id]/page.tsx index 9c4cef6..3753de3 100644 --- a/lynx/dashboard/ui/src/app/[locale]/(dashboard)/app/organizations/[id]/projects/[proj_id]/page.tsx +++ b/lynx/dashboard/ui/src/app/[locale]/(dashboard)/app/organizations/[id]/projects/[proj_id]/page.tsx @@ -1,56 +1,52 @@ +import { ChevronRight } from "lucide-react"; import { cookies } from "next/headers"; +import Link from "next/link"; import { notFound } from "next/navigation"; import { getTranslations } from "next-intl/server"; -import { BACKEND_URL } from "@/lib/api"; -import Link from "next/link"; -import { ChevronRight } from "lucide-react"; -import { ResourceForm } from "@/components/(dashboard)/app/organizations/[id]/projects/[proj_id]/ResourceForm"; import { ContainerCard } from "@/components/(dashboard)/app/organizations/[id]/projects/[proj_id]/ContainerCard"; import { DeployForm } from "@/components/(dashboard)/app/organizations/[id]/projects/[proj_id]/DeployForm"; import { HorizontalScaleSection } from "@/components/(dashboard)/app/organizations/[id]/projects/[proj_id]/HorizontalScaleSection"; +import { ResourceForm } from "@/components/(dashboard)/app/organizations/[id]/projects/[proj_id]/ResourceForm"; +import { BACKEND_URL } from "@/lib/api"; interface Project { + agent_id: string; + created_at: string; id: string; name: string; - slug: string; - agent_id: string; organization_id: string; - created_at: string; + slug: string; } interface Container { - Names: string[]; Image: string; - Status: string; + Names: string[]; State: string; + Status: string; } interface Agent { id: string; name: string; - wg_ip: string; status: string; + wg_ip: string; } interface Tunnel { - id: string; - agent_b_id: string; agent_a_wg_ip: string; + agent_b_id: string; agent_b_wg_ip: string; + id: string; replica_count: number; status: string; } -async function fetchProject( - token: string, - orgId: string, - projId: string, -): Promise { +async function fetchProject(token: string, orgId: string, projId: string): Promise { try { - const res = await fetch( - `${BACKEND_URL}/organizations/${orgId}/projects/${projId}`, - { headers: { Authorization: `Bearer ${token}` }, cache: "no-store" }, - ); + const res = await fetch(`${BACKEND_URL}/organizations/${orgId}/projects/${projId}`, { + cache: "no-store", + headers: { Authorization: `Bearer ${token}` }, + }); if (!res.ok) return null; return res.json(); } catch { @@ -61,8 +57,8 @@ async function fetchProject( async function fetchAgents(token: string): Promise { try { const res = await fetch(`${BACKEND_URL}/agents`, { - headers: { Authorization: `Bearer ${token}` }, cache: "no-store", + headers: { Authorization: `Bearer ${token}` }, }); if (!res.ok) return []; return res.json(); @@ -71,16 +67,12 @@ async function fetchAgents(token: string): Promise { } } -async function fetchTunnels( - token: string, - orgId: string, - projId: string, -): Promise { +async function fetchTunnels(token: string, orgId: string, projId: string): Promise { try { - const res = await fetch( - `${BACKEND_URL}/organizations/${orgId}/projects/${projId}/scale/horizontal`, - { headers: { Authorization: `Bearer ${token}` }, cache: "no-store" }, - ); + const res = await fetch(`${BACKEND_URL}/organizations/${orgId}/projects/${projId}/scale/horizontal`, { + cache: "no-store", + headers: { Authorization: `Bearer ${token}` }, + }); if (!res.ok) return []; return res.json(); } catch { @@ -88,16 +80,12 @@ async function fetchTunnels( } } -async function fetchContainers( - token: string, - orgId: string, - projId: string, -): Promise { +async function fetchContainers(token: string, orgId: string, projId: string): Promise { try { - const res = await fetch( - `${BACKEND_URL}/organizations/${orgId}/projects/${projId}/containers`, - { headers: { Authorization: `Bearer ${token}` }, cache: "no-store" }, - ); + const res = await fetch(`${BACKEND_URL}/organizations/${orgId}/projects/${projId}/containers`, { + cache: "no-store", + headers: { Authorization: `Bearer ${token}` }, + }); if (!res.ok) return []; const data = (await res.json()) as { containers?: Container[] } | Container[]; return Array.isArray(data) ? data : (data.containers ?? []); @@ -112,10 +100,7 @@ export default async function ProjectDetailPage({ params: Promise<{ locale: string; id: string; proj_id: string }>; }) { const { locale, id: orgId, proj_id: projId } = await params; - const [t, jar] = await Promise.all([ - getTranslations({ locale, namespace: "app.projects" }), - cookies(), - ]); + const [t, jar] = await Promise.all([getTranslations({ locale, namespace: "app.projects" }), cookies()]); const tok = jar.get("access_token")?.value ?? ""; const [project, containers, tunnels, agents] = await Promise.all([ @@ -128,27 +113,24 @@ export default async function ProjectDetailPage({ if (!project) notFound(); const containerLabels = { + error: t("cActionError"), + remove: t("cRemove"), + restart: t("cRestart"), start: t("cStart"), stop: t("cStop"), - restart: t("cRestart"), - remove: t("cRemove"), success: t("cActionSuccess"), - error: t("cActionError"), }; return (
); diff --git a/lynx/dashboard/ui/src/app/[locale]/(dashboard)/app/settings/page.tsx b/lynx/dashboard/ui/src/app/[locale]/(dashboard)/app/settings/page.tsx index 2d92809..25d4fec 100644 --- a/lynx/dashboard/ui/src/app/[locale]/(dashboard)/app/settings/page.tsx +++ b/lynx/dashboard/ui/src/app/[locale]/(dashboard)/app/settings/page.tsx @@ -1,54 +1,54 @@ -import { Suspense } from "react"; import { cookies } from "next/headers"; import { getTranslations } from "next-intl/server"; -import { BACKEND_URL } from "@/lib/api"; -import { SessionList } from "@/components/(dashboard)/app/settings/SessionList"; -import { SessionListSkeleton } from "@/components/(dashboard)/app/settings/SessionListSkeleton"; -import { RotateButton } from "@/components/(dashboard)/app/settings/RotateButton"; +import { Suspense } from "react"; +import { getMigrationStatus } from "@/actions/(dashboard)/app/settings/migration"; +import { getMe } from "@/actions/(dashboard)/app/settings/profile"; import { BrandingForm } from "@/components/(dashboard)/app/settings/BrandingForm"; -import { UpdateSection } from "@/components/(dashboard)/app/settings/UpdateSection"; +import { ChangePasswordForm } from "@/components/(dashboard)/app/settings/ChangePasswordForm"; import { DomainSection } from "@/components/(dashboard)/app/settings/DomainSection"; import { MigrationSection } from "@/components/(dashboard)/app/settings/MigrationSection"; -import { getMigrationStatus } from "@/actions/(dashboard)/app/settings/migration"; -import { ChangePasswordForm } from "@/components/(dashboard)/app/settings/ChangePasswordForm"; -import { SingleSessionToggle } from "@/components/(dashboard)/app/settings/SingleSessionToggle"; -import { getMe } from "@/actions/(dashboard)/app/settings/profile"; +import { RotateButton } from "@/components/(dashboard)/app/settings/RotateButton"; import { RotationLog } from "@/components/(dashboard)/app/settings/RotationLog"; +import { SessionList } from "@/components/(dashboard)/app/settings/SessionList"; +import { SessionListSkeleton } from "@/components/(dashboard)/app/settings/SessionListSkeleton"; +import { SingleSessionToggle } from "@/components/(dashboard)/app/settings/SingleSessionToggle"; +import { UpdateSection } from "@/components/(dashboard)/app/settings/UpdateSection"; +import { BACKEND_URL } from "@/lib/api"; interface Branding { + accent_color: string; company_name: string; logo_url: string | null; primary_color: string; secondary_color: string; - accent_color: string; } interface DomainConfig { - domain: string | null; - cert_type: string; cert_expires_at: string | null; + cert_type: string; + domain: string | null; + error_message: string | null; hsts_enabled: boolean; port_19443_open: boolean; status: string; - error_message: string | null; } const BRANDING_DEFAULTS: Branding = { + accent_color: "#6366f1", company_name: "Lynx", logo_url: null, primary_color: "#0f172a", secondary_color: "#38bdf8", - accent_color: "#6366f1", }; const DOMAIN_DEFAULTS: DomainConfig = { - domain: null, - cert_type: "self_signed", cert_expires_at: null, + cert_type: "self_signed", + domain: null, + error_message: null, hsts_enabled: false, port_19443_open: true, status: "unconfigured", - error_message: null, }; async function fetchBranding(): Promise { @@ -66,8 +66,8 @@ async function fetchBranding(): Promise { async function fetchDomainConfig(token: string): Promise { try { const res = await fetch(`${BACKEND_URL}/domain`, { - headers: { Authorization: `Bearer ${token}` }, cache: "no-store", + headers: { Authorization: `Bearer ${token}` }, }); if (!res.ok) return DOMAIN_DEFAULTS; return (await res.json()) as DomainConfig; @@ -76,9 +76,7 @@ async function fetchDomainConfig(token: string): Promise { } } -export default async function SettingsPage({ - params, -}: { params: Promise<{ locale: string }> }) { +export default async function SettingsPage({ params }: { params: Promise<{ locale: string }> }) { const { locale } = await params; const [t, jar, branding] = await Promise.all([ getTranslations({ locale, namespace: "app.settings" }), @@ -109,25 +107,25 @@ export default async function SettingsPage({

{t("changePassword")}

@@ -136,128 +134,110 @@ export default async function SettingsPage({ )}
-

- {t("domain")} -

+

{t("domain")}

-

- {t("security")} -

+

{t("security")}

{t("rotateKeys")}

-

- {t("rotateKeysDesc")} -

+

{t("rotateKeysDesc")}

- +
-

- {t("rotationLog")} -

+

{t("rotationLog")}

}> - +
-

- {t("updates")} -

+

{t("updates")}

-

- {t("updatesDesc")} -

+

{t("updatesDesc")}

-

- {t("branding")} -

+

{t("branding")}

@@ -272,36 +252,39 @@ export default async function SettingsPage({
@@ -309,11 +292,9 @@ export default async function SettingsPage({ )}
-

- {t("sessions")} -

+

{t("sessions")}

}> - +
diff --git a/lynx/dashboard/ui/src/app/[locale]/layout.tsx b/lynx/dashboard/ui/src/app/[locale]/layout.tsx index c0986d3..214745e 100644 --- a/lynx/dashboard/ui/src/app/[locale]/layout.tsx +++ b/lynx/dashboard/ui/src/app/[locale]/layout.tsx @@ -1,35 +1,39 @@ -import { Geist, Geist_Mono } from "next/font/google"; +import type { Metadata } from "next"; +import { Geist_Mono, Poppins } from "next/font/google"; +import { cookies } from "next/headers"; +import { notFound } from "next/navigation"; import { NextIntlClientProvider } from "next-intl"; import { getMessages } from "next-intl/server"; -import { notFound } from "next/navigation"; -import { cookies } from "next/headers"; -import type { Metadata } from "next"; -import { Toaster } from "@/components/ui/sonner"; import { ThemeProvider } from "@/components/ThemeProvider"; +import { Toaster } from "@/components/ui/sonner"; import { routing } from "@/i18n/routing"; import { BACKEND_URL } from "@/lib/api"; import "../globals.css"; -const geistSans = Geist({ variable: "--font-geist-sans", subsets: ["latin"] }); +const poppins = Poppins({ + subsets: ["latin"], + variable: "--font-sans", + weight: ["300", "400", "500", "600", "700"], +}); const geistMono = Geist_Mono({ - variable: "--font-geist-mono", subsets: ["latin"], + variable: "--font-geist-mono", }); interface Branding { + accent_color: string; company_name: string; logo_url: string | null; primary_color: string; secondary_color: string; - accent_color: string; } const BRANDING_DEFAULTS: Branding = { + accent_color: "#6366f1", company_name: "Lynx", logo_url: null, primary_color: "#0f172a", secondary_color: "#38bdf8", - accent_color: "#6366f1", }; async function fetchBranding(): Promise { @@ -44,17 +48,13 @@ async function fetchBranding(): Promise { } } -export async function generateMetadata({ - params, -}: { - params: Promise<{ locale: string }>; -}): Promise { +export async function generateMetadata({ params }: { params: Promise<{ locale: string }> }): Promise { await params; const branding = await fetchBranding(); return { - title: branding.company_name, description: "Distributed infrastructure orchestration", - robots: { index: false, follow: false }, + robots: { follow: false, index: false }, + title: branding.company_name, }; } @@ -76,22 +76,19 @@ export default async function LocaleLayout({ // Falls back to "system" if not set. const defaultTheme = jar.get("theme_preference")?.value ?? "system"; - const [messages, branding] = await Promise.all([ - getMessages(), - fetchBranding(), - ]); + const [messages, branding] = await Promise.all([getMessages(), fetchBranding()]); const brandVars = { + "--brand-accent": branding.accent_color, "--brand-primary": branding.primary_color, "--brand-secondary": branding.secondary_color, - "--brand-accent": branding.accent_color, } as React.CSSProperties; return ( diff --git a/lynx/dashboard/ui/src/app/[locale]/page.tsx b/lynx/dashboard/ui/src/app/[locale]/page.tsx index 28971f8..a10df25 100644 --- a/lynx/dashboard/ui/src/app/[locale]/page.tsx +++ b/lynx/dashboard/ui/src/app/[locale]/page.tsx @@ -1,10 +1,6 @@ import { redirect } from "next/navigation"; -export default async function LocaleRootPage({ - params, -}: { - params: Promise<{ locale: string }>; -}) { +export default async function LocaleRootPage({ params }: { params: Promise<{ locale: string }> }) { const { locale } = await params; redirect(`/${locale}/login`); } diff --git a/lynx/dashboard/ui/src/app/globals.css b/lynx/dashboard/ui/src/app/globals.css index ed29b07..16a248f 100644 --- a/lynx/dashboard/ui/src/app/globals.css +++ b/lynx/dashboard/ui/src/app/globals.css @@ -5,47 +5,46 @@ @custom-variant dark (&:is(.dark *)); @theme inline { - --color-background: var(--background); - --color-foreground: var(--foreground); - --font-sans: var(--font-sans); - --font-mono: var(--font-geist-mono); - --font-heading: var(--font-sans); - --color-sidebar-ring: var(--sidebar-ring); - --color-sidebar-border: var(--sidebar-border); - --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); - --color-sidebar-accent: var(--sidebar-accent); - --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); - --color-sidebar-primary: var(--sidebar-primary); - --color-sidebar-foreground: var(--sidebar-foreground); - --color-sidebar: var(--sidebar); - --color-chart-5: var(--chart-5); - --color-chart-4: var(--chart-4); - --color-chart-3: var(--chart-3); - --color-chart-2: var(--chart-2); - --color-chart-1: var(--chart-1); - --color-ring: var(--ring); - --color-input: var(--input); - --color-border: var(--border); - --color-destructive: var(--destructive); - --color-accent-foreground: var(--accent-foreground); - --color-accent: var(--accent); - --color-muted-foreground: var(--muted-foreground); - --color-muted: var(--muted); - --color-secondary-foreground: var(--secondary-foreground); - --color-secondary: var(--secondary); - --color-primary-foreground: var(--primary-foreground); - --color-primary: var(--primary); - --color-popover-foreground: var(--popover-foreground); - --color-popover: var(--popover); - --color-card-foreground: var(--card-foreground); - --color-card: var(--card); - --radius-sm: calc(var(--radius) * 0.6); - --radius-md: calc(var(--radius) * 0.8); - --radius-lg: var(--radius); - --radius-xl: calc(var(--radius) * 1.4); - --radius-2xl: calc(var(--radius) * 1.8); - --radius-3xl: calc(var(--radius) * 2.2); - --radius-4xl: calc(var(--radius) * 2.6); + --color-background: var(--background); + --color-foreground: var(--foreground); + --font-mono: var(--font-geist-mono); + --font-heading: var(--font-sans); + --color-sidebar-ring: var(--sidebar-ring); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar: var(--sidebar); + --color-chart-5: var(--chart-5); + --color-chart-4: var(--chart-4); + --color-chart-3: var(--chart-3); + --color-chart-2: var(--chart-2); + --color-chart-1: var(--chart-1); + --color-ring: var(--ring); + --color-input: var(--input); + --color-border: var(--border); + --color-destructive: var(--destructive); + --color-accent-foreground: var(--accent-foreground); + --color-accent: var(--accent); + --color-muted-foreground: var(--muted-foreground); + --color-muted: var(--muted); + --color-secondary-foreground: var(--secondary-foreground); + --color-secondary: var(--secondary); + --color-primary-foreground: var(--primary-foreground); + --color-primary: var(--primary); + --color-popover-foreground: var(--popover-foreground); + --color-popover: var(--popover); + --color-card-foreground: var(--card-foreground); + --color-card: var(--card); + --radius-sm: calc(var(--radius) * 0.6); + --radius-md: calc(var(--radius) * 0.8); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) * 1.4); + --radius-2xl: calc(var(--radius) * 1.8); + --radius-3xl: calc(var(--radius) * 2.2); + --radius-4xl: calc(var(--radius) * 2.6); } :root { diff --git a/lynx/dashboard/ui/src/app/layout.tsx b/lynx/dashboard/ui/src/app/layout.tsx index c6795cf..44d5522 100644 --- a/lynx/dashboard/ui/src/app/layout.tsx +++ b/lynx/dashboard/ui/src/app/layout.tsx @@ -1,7 +1,3 @@ -export default function RootLayout({ - children, -}: { - children: React.ReactNode; -}) { +export default function RootLayout({ children }: { children: React.ReactNode }) { return children; } diff --git a/lynx/dashboard/ui/src/components/(auth)/login/LoginForm.tsx b/lynx/dashboard/ui/src/components/(auth)/login/LoginForm.tsx index 36028a9..02c2598 100644 --- a/lynx/dashboard/ui/src/components/(auth)/login/LoginForm.tsx +++ b/lynx/dashboard/ui/src/components/(auth)/login/LoginForm.tsx @@ -1,16 +1,16 @@ "use client"; -import { useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; -import { useTranslations } from "next-intl"; -import { useRouter } from "next/navigation"; import Link from "next/link"; +import { useRouter } from "next/navigation"; +import { useTranslations } from "next-intl"; +import { useForm } from "react-hook-form"; import { toast } from "sonner"; +import { loginAction } from "@/actions/(auth)/login"; import { Button } from "@/components/ui/button"; +import { Field, FieldError, FieldLabel } from "@/components/ui/field"; import { Input } from "@/components/ui/input"; -import { Field, FieldLabel, FieldError } from "@/components/ui/field"; -import { loginSchema, type LoginInput } from "@/schemas/(auth)/login"; -import { loginAction } from "@/actions/(auth)/login"; +import { type LoginInput, loginSchema } from "@/schemas/(auth)/login"; type Props = { locale: string }; @@ -33,13 +33,13 @@ export function LoginForm({ locale }: Props) { }); toast.promise(promise, { - loading: t("submitting"), - success: t("submit"), error: (e: Error) => { if (e.message === "rateLimited") return t("rateLimited", { minutes: 15 }); if (e.message === "invalidCredentials") return t("invalidCredentials"); return t("serverError"); }, + loading: t("submitting"), + success: t("submit"), }); try { @@ -55,15 +55,15 @@ export function LoginForm({ locale }: Props) { }; return ( -
+ {t("username")} @@ -75,21 +75,21 @@ export function LoginForm({ locale }: Props) { type="password" {...register("password")} autoComplete="current-password" - disabled={isSubmitting} className="h-10" + disabled={isSubmitting} /> -

{t("noAccount")}{" "} {t("register")} diff --git a/lynx/dashboard/ui/src/components/(auth)/register/RegisterForm.tsx b/lynx/dashboard/ui/src/components/(auth)/register/RegisterForm.tsx index 94cd5e8..5cf7176 100644 --- a/lynx/dashboard/ui/src/components/(auth)/register/RegisterForm.tsx +++ b/lynx/dashboard/ui/src/components/(auth)/register/RegisterForm.tsx @@ -1,16 +1,16 @@ "use client"; -import { useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; -import { useTranslations } from "next-intl"; -import { useRouter } from "next/navigation"; import Link from "next/link"; +import { useRouter } from "next/navigation"; +import { useTranslations } from "next-intl"; +import { useForm } from "react-hook-form"; import { toast } from "sonner"; +import { registerAction } from "@/actions/(auth)/register"; import { Button } from "@/components/ui/button"; +import { Field, FieldError, FieldLabel } from "@/components/ui/field"; import { Input } from "@/components/ui/input"; -import { Field, FieldLabel, FieldError } from "@/components/ui/field"; -import { registerSchema, type RegisterInput } from "@/schemas/(auth)/register"; -import { registerAction } from "@/actions/(auth)/register"; +import { type RegisterInput, registerSchema } from "@/schemas/(auth)/register"; type Props = { locale: string }; @@ -33,13 +33,13 @@ export function RegisterForm({ locale }: Props) { }); toast.promise(promise, { - loading: t("submitting"), - success: t("submit"), error: (e: Error) => { if (e.message === "usernameTaken") return t("usernameTaken"); if (e.message === "emailTaken") return t("emailTaken"); return t("serverError"); }, + loading: t("submitting"), + success: t("submit"), }); try { @@ -51,15 +51,15 @@ export function RegisterForm({ locale }: Props) { }; return ( - + {t("username")} @@ -71,8 +71,8 @@ export function RegisterForm({ locale }: Props) { type="email" {...register("email")} autoComplete="email" - disabled={isSubmitting} className="h-10" + disabled={isSubmitting} /> @@ -84,21 +84,21 @@ export function RegisterForm({ locale }: Props) { type="password" {...register("password")} autoComplete="new-password" - disabled={isSubmitting} className="h-10" + disabled={isSubmitting} /> -

{t("hasAccount")}{" "} {t("login")} diff --git a/lynx/dashboard/ui/src/components/(dashboard)/app/LocaleSwitcher.tsx b/lynx/dashboard/ui/src/components/(dashboard)/app/LocaleSwitcher.tsx index 3a84277..fe94403 100644 --- a/lynx/dashboard/ui/src/components/(dashboard)/app/LocaleSwitcher.tsx +++ b/lynx/dashboard/ui/src/components/(dashboard)/app/LocaleSwitcher.tsx @@ -1,20 +1,20 @@ "use client"; +import Image from "next/image"; import { usePathname, useRouter } from "next/navigation"; import { useTransition } from "react"; -import Image from "next/image"; +import { updateLocaleAction } from "@/actions/(dashboard)/app/settings/preferences"; +import { Button } from "@/components/ui/button"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; -import { Button } from "@/components/ui/button"; -import { updateLocaleAction } from "@/actions/(dashboard)/app/settings/preferences"; const LOCALES: { code: string; label: string; flag: string }[] = [ - { code: "en", label: "English", flag: "/flags/en.svg" }, - { code: "es", label: "Español", flag: "/flags/es.svg" }, + { code: "en", flag: "/flags/en.svg", label: "English" }, + { code: "es", flag: "/flags/es.svg", label: "Español" }, ]; type Props = { locale: string }; @@ -40,32 +40,26 @@ export function LocaleSwitcher({ locale }: Props) { {LOCALES.map(({ code, label, flag }) => ( - handleSelect(code)} - > - {label} + handleSelect(code)}> + {label} {label} - {locale === code && ( - - )} + {locale === code && } ))} diff --git a/lynx/dashboard/ui/src/components/(dashboard)/app/NotificationBell.tsx b/lynx/dashboard/ui/src/components/(dashboard)/app/NotificationBell.tsx index 4e8d9ac..a7db3b6 100644 --- a/lynx/dashboard/ui/src/components/(dashboard)/app/NotificationBell.tsx +++ b/lynx/dashboard/ui/src/components/(dashboard)/app/NotificationBell.tsx @@ -1,14 +1,10 @@ "use client"; -import { useCallback, useState } from "react"; import { Bell } from "lucide-react"; -import { useAgentEvents, type AgentEvent, type AgentEventKind } from "@/lib/useAgentEvents"; import { useTranslations } from "next-intl"; -import { - Popover, - PopoverContent, - PopoverTrigger, -} from "@/components/ui/popover"; +import { useCallback, useState } from "react"; +import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; +import { type AgentEvent, type AgentEventKind, useAgentEvents } from "@/lib/useAgentEvents"; const ALERT_EVENTS: AgentEventKind[] = [ "heartbeat_lost", @@ -18,11 +14,11 @@ const ALERT_EVENTS: AgentEventKind[] = [ ]; interface Notification { - id: string; agent_id: string; - event: AgentEventKind; - detail: string | null; at: Date; + detail: string | null; + event: AgentEventKind; + id: string; } export function NotificationBell() { @@ -34,11 +30,11 @@ export function NotificationBell() { if (!ALERT_EVENTS.includes(evt.event)) return; setNotifications((prev) => [ { - id: crypto.randomUUID(), agent_id: evt.agent_id, - event: evt.event, - detail: evt.detail, at: new Date(), + detail: evt.detail, + event: evt.event, + id: crypto.randomUUID(), }, ...prev.slice(0, 49), // keep last 50 ]); @@ -49,12 +45,12 @@ export function NotificationBell() { const unread = notifications.length; return ( - + @@ -79,26 +75,21 @@ export function NotificationBell() {

{notifications.length === 0 ? ( -

- {t("empty")} -

+

{t("empty")}

) : ( notifications.map((n) => (
- - {t(`event.${n.event}`)} - + {t(`event.${n.event}`)} {n.at.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })}

- {n.agent_id.slice(0, 8)}… - {n.detail && ` · ${n.detail}`} + {n.agent_id.slice(0, 8)}…{n.detail && ` · ${n.detail}`}

)) diff --git a/lynx/dashboard/ui/src/components/(dashboard)/app/Sidebar.tsx b/lynx/dashboard/ui/src/components/(dashboard)/app/Sidebar.tsx index 67a8e38..a07acc3 100644 --- a/lynx/dashboard/ui/src/components/(dashboard)/app/Sidebar.tsx +++ b/lynx/dashboard/ui/src/components/(dashboard)/app/Sidebar.tsx @@ -1,22 +1,15 @@ "use client"; -import { Button } from "@/components/ui/button"; -import { - Building2, - LayoutDashboard, - LogOut, - Monitor, - Settings, - ShieldCheck, -} from "lucide-react"; -import { useTranslations } from "next-intl"; +import { Building2, LayoutDashboard, LogOut, Monitor, Settings, ShieldCheck } from "lucide-react"; import Link from "next/link"; import { usePathname } from "next/navigation"; +import { useTranslations } from "next-intl"; import { useTransition } from "react"; import { logoutAction } from "@/actions/(dashboard)/app/logout"; +import { Button } from "@/components/ui/button"; +import { LocaleSwitcher } from "./LocaleSwitcher"; import { NotificationBell } from "./NotificationBell"; import { ThemeToggle } from "./ThemeToggle"; -import { LocaleSwitcher } from "./LocaleSwitcher"; type Props = { locale: string; companyName: string; logoUrl: string | null; isAdmin: boolean }; @@ -28,27 +21,25 @@ export function Sidebar({ locale, companyName, logoUrl, isAdmin }: Props) { const items = [ { href: `/${locale}/app`, - label: t("overview"), icon: LayoutDashboard, + label: t("overview"), }, { href: `/${locale}/app/agents`, - label: t("agents"), icon: Monitor, + label: t("agents"), }, { href: `/${locale}/app/organizations`, - label: t("organizations"), icon: Building2, + label: t("organizations"), }, { href: `/${locale}/app/settings`, - label: t("settings"), icon: Settings, + label: t("settings"), }, - ...(isAdmin - ? [{ href: `/${locale}/app/admin`, label: t("admin"), icon: ShieldCheck }] - : []), + ...(isAdmin ? [{ href: `/${locale}/app/admin`, icon: ShieldCheck, label: t("admin") }] : []), ]; return ( @@ -57,7 +48,7 @@ export function Sidebar({ locale, companyName, logoUrl, isAdmin }: Props) {
{logoUrl ? ( // eslint-disable-next-line @next/next/no-img-element - {companyName} + {companyName} ) : ( {items.map(({ href, label, icon: Icon }) => { - const active = - href === `/${locale}/app` - ? pathname === href - : pathname.startsWith(href); + const active = href === `/${locale}/app` ? pathname === href : pathname.startsWith(href); return ( {label} @@ -97,12 +85,10 @@ export function Sidebar({ locale, companyName, logoUrl, isAdmin }: Props) {
{THEMES.map(({ value, icon: ItemIcon, label }) => ( - handleSelect(value)} - > + handleSelect(value)}> {label} - {theme === value && ( - - )} + {theme === value && } ))} diff --git a/lynx/dashboard/ui/src/components/(dashboard)/app/admin/RolesPanel.tsx b/lynx/dashboard/ui/src/components/(dashboard)/app/admin/RolesPanel.tsx index 404081f..af7c19d 100644 --- a/lynx/dashboard/ui/src/components/(dashboard)/app/admin/RolesPanel.tsx +++ b/lynx/dashboard/ui/src/components/(dashboard)/app/admin/RolesPanel.tsx @@ -1,26 +1,20 @@ "use client"; +import { Plus, Trash2 } from "lucide-react"; import { useState, useTransition } from "react"; import { toast } from "sonner"; import { addRolePermissionAction, createRoleAction, deleteRoleAction, - removeRolePermissionAction, type PermRef, type RoleRow, + removeRolePermissionAction, } from "@/actions/(dashboard)/app/admin/users"; +import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; -import { Badge } from "@/components/ui/badge"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select"; -import { Plus, Trash2 } from "lucide-react"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; type Props = { initial: RoleRow[]; @@ -85,9 +79,7 @@ export function RolesPanel({ initial, allPerms, labels }: Props) { if (success) { setRoles((prev) => prev.map((r) => - r.id === roleId - ? { ...r, permissions: [...r.permissions, { id: perm.id, key: perm.key }] } - : r, + r.id === roleId ? { ...r, permissions: [...r.permissions, { id: perm.id, key: perm.key }] } : r, ), ); toast.success(labels.addPermissionSuccess); @@ -103,9 +95,7 @@ export function RolesPanel({ initial, allPerms, labels }: Props) { if (res.success) { setRoles((prev) => prev.map((r) => - r.id === roleId - ? { ...r, permissions: r.permissions.filter((p) => p.id !== permId) } - : r, + r.id === roleId ? { ...r, permissions: r.permissions.filter((p) => p.id !== permId) } : r, ), ); toast.success(labels.removePermissionSuccess); @@ -138,9 +128,7 @@ export function RolesPanel({ initial, allPerms, labels }: Props) {
{roles.map((role) => { - const assignablePerms = allPerms.filter( - (p) => !role.permissions.some((rp) => rp.id === p.id), - ); + const assignablePerms = allPerms.filter((p) => !role.permissions.some((rp) => rp.id === p.id)); return (
diff --git a/lynx/dashboard/ui/src/components/(dashboard)/app/admin/UsersPanel.tsx b/lynx/dashboard/ui/src/components/(dashboard)/app/admin/UsersPanel.tsx index 9209409..40b0c49 100644 --- a/lynx/dashboard/ui/src/components/(dashboard)/app/admin/UsersPanel.tsx +++ b/lynx/dashboard/ui/src/components/(dashboard)/app/admin/UsersPanel.tsx @@ -1,17 +1,18 @@ "use client"; +import { MoreHorizontal, ShieldAlert, Trash2, UserPlus } from "lucide-react"; import { useState, useTransition } from "react"; import { toast } from "sonner"; import { addUserRoleAction, deleteUserAction, forcePasswordChangeAction, - removeUserRoleAction, type RoleRow, + removeUserRoleAction, type UserRow, } from "@/actions/(dashboard)/app/admin/users"; -import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; import { DropdownMenu, DropdownMenuContent, @@ -19,14 +20,7 @@ import { DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select"; -import { MoreHorizontal, ShieldAlert, Trash2, UserPlus } from "lucide-react"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; type Props = { initial: UserRow[]; @@ -71,11 +65,7 @@ export function UsersPanel({ initial, roles, labels }: Props) { startTransition(async () => { const { success } = await forcePasswordChangeAction(userId); if (success) { - setUsers((prev) => - prev.map((u) => - u.id === userId ? { ...u, force_password_change: true } : u, - ), - ); + setUsers((prev) => prev.map((u) => (u.id === userId ? { ...u, force_password_change: true } : u))); toast.success(labels.forcePasswordChangeSuccess); } else { toast.error(labels.forcePasswordChangeError); @@ -91,9 +81,7 @@ export function UsersPanel({ initial, roles, labels }: Props) { if (success) { setUsers((prev) => prev.map((u) => - u.id === userId - ? { ...u, roles: [...u.roles, { id: role.id, name: role.name }] } - : u, + u.id === userId ? { ...u, roles: [...u.roles, { id: role.id, name: role.name }] } : u, ), ); toast.success(labels.addRoleSuccess); @@ -108,11 +96,7 @@ export function UsersPanel({ initial, roles, labels }: Props) { const { success } = await removeUserRoleAction(userId, roleId); if (success) { setUsers((prev) => - prev.map((u) => - u.id === userId - ? { ...u, roles: u.roles.filter((r) => r.id !== roleId) } - : u, - ), + prev.map((u) => (u.id === userId ? { ...u, roles: u.roles.filter((r) => r.id !== roleId) } : u)), ); toast.success(labels.removeRoleSuccess); } else { @@ -124,9 +108,7 @@ export function UsersPanel({ initial, roles, labels }: Props) { return (
{users.map((user) => { - const assignableRoles = roles.filter( - (r) => !user.roles.some((ur) => ur.id === r.id), - ); + const assignableRoles = roles.filter((r) => !user.roles.some((ur) => ur.id === r.id)); return (
@@ -138,11 +120,7 @@ export function UsersPanel({ initial, roles, labels }: Props) { )} - diff --git a/lynx/dashboard/ui/src/components/(dashboard)/app/agents/AgentList.tsx b/lynx/dashboard/ui/src/components/(dashboard)/app/agents/AgentList.tsx index a78279e..ba1e07c 100644 --- a/lynx/dashboard/ui/src/components/(dashboard)/app/agents/AgentList.tsx +++ b/lynx/dashboard/ui/src/components/(dashboard)/app/agents/AgentList.tsx @@ -1,8 +1,8 @@ +import Link from "next/link"; import { getTranslations } from "next-intl/server"; -import { BACKEND_URL } from "@/lib/api"; import { Badge } from "@/components/ui/badge"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; -import Link from "next/link"; +import { BACKEND_URL } from "@/lib/api"; import { NftablesAlert } from "./NftablesAlert"; type Agent = { @@ -15,8 +15,8 @@ type Agent = { }; interface NftStatus { - diverged: boolean; detail?: string | null; + diverged: boolean; } async function fetchAgents(token: string): Promise { @@ -36,8 +36,8 @@ async function fetchAgents(token: string): Promise { async function fetchNftStatus(token: string, agentId: string): Promise { try { const res = await fetch(`${BACKEND_URL}/agents/${agentId}/nftables-status`, { - headers: { Authorization: `Bearer ${token}` }, cache: "no-store", + headers: { Authorization: `Bearer ${token}` }, }); if (!res.ok) return { diverged: false }; return res.json(); @@ -46,13 +46,10 @@ async function fetchNftStatus(token: string, agentId: string): Promise = { - online: "default", +const STATUS_BADGE: Record = { lockdown: "destructive", offline: "secondary", + online: "default", }; function formatHeartbeat(ts: string | null): string { @@ -63,24 +60,13 @@ function formatHeartbeat(ts: string | null): string { return `${Math.floor(diff / 3600)}h ago`; } -export async function AgentList({ - token, - locale, -}: { - token: string; - locale: string; -}) { - const [agents, t] = await Promise.all([ - fetchAgents(token), - getTranslations({ locale, namespace: "app.agents" }), - ]); +export async function AgentList({ token, locale }: { token: string; locale: string }) { + const [agents, t] = await Promise.all([fetchAgents(token), getTranslations({ locale, namespace: "app.agents" })]); // Fetch nftables divergence status for online agents in parallel const nftStatuses = await Promise.all( agents.map((a) => - a.status === "online" - ? fetchNftStatus(token, a.id) - : Promise.resolve({ diverged: false } as NftStatus), + a.status === "online" ? fetchNftStatus(token, a.id) : Promise.resolve({ diverged: false } as NftStatus), ), ); @@ -89,9 +75,7 @@ export async function AgentList({

{t("noAgents")}

-

- {t("noAgentsDesc")} -

+

{t("noAgentsDesc")}

); @@ -105,44 +89,33 @@ export async function AgentList({
- - {agent.name} - - - {t(`status.${agent.status}`)} - + {agent.name} + {t(`status.${agent.status}`)}

- - {t("wgIp")} - {" "} - {agent.wg_ip} + {t("wgIp")} {agent.wg_ip}

- - {t("version")} - {" "} + {t("version")}{" "} {agent.version ?? "—"}

- - {t("lastHeartbeat")} - {" "} + {t("lastHeartbeat")}{" "} {formatHeartbeat(agent.last_heartbeat)}

{agent.id}

{t("detailTitle")} → {t("auditLog")} @@ -152,12 +125,12 @@ export async function AgentList({ agentId={agent.id} detail={nft.detail ?? null} labels={{ - title: t("nftDiverged"), - restore: t("nftRestore"), accept: t("nftAccept"), - restoreSuccess: t("nftRestoreSuccess"), acceptSuccess: t("nftAcceptSuccess"), error: t("nftError"), + restore: t("nftRestore"), + restoreSuccess: t("nftRestoreSuccess"), + title: t("nftDiverged"), }} /> )} diff --git a/lynx/dashboard/ui/src/components/(dashboard)/app/agents/AgentListSkeleton.tsx b/lynx/dashboard/ui/src/components/(dashboard)/app/agents/AgentListSkeleton.tsx index 8553db5..0fd642e 100644 --- a/lynx/dashboard/ui/src/components/(dashboard)/app/agents/AgentListSkeleton.tsx +++ b/lynx/dashboard/ui/src/components/(dashboard)/app/agents/AgentListSkeleton.tsx @@ -1,5 +1,5 @@ -import { Skeleton } from "@/components/ui/skeleton"; import { Card, CardContent, CardHeader } from "@/components/ui/card"; +import { Skeleton } from "@/components/ui/skeleton"; export function AgentListSkeleton() { return ( diff --git a/lynx/dashboard/ui/src/components/(dashboard)/app/agents/NftablesAlert.tsx b/lynx/dashboard/ui/src/components/(dashboard)/app/agents/NftablesAlert.tsx index 140ad2a..53aa81d 100644 --- a/lynx/dashboard/ui/src/components/(dashboard)/app/agents/NftablesAlert.tsx +++ b/lynx/dashboard/ui/src/components/(dashboard)/app/agents/NftablesAlert.tsx @@ -1,10 +1,10 @@ "use client"; +import { AlertTriangle } from "lucide-react"; import { useTransition } from "react"; import { toast } from "sonner"; -import { AlertTriangle } from "lucide-react"; -import { Button } from "@/components/ui/button"; import { resolveNftables } from "@/actions/(dashboard)/app/agents"; +import { Button } from "@/components/ui/button"; interface Props { agentId: string; @@ -26,9 +26,7 @@ export function NftablesAlert({ agentId, detail, labels }: Props) { startTransition(async () => { const result = await resolveNftables(agentId, action); if (result.ok) { - toast.success( - action === "restore" ? labels.restoreSuccess : labels.acceptSuccess, - ); + toast.success(action === "restore" ? labels.restoreSuccess : labels.acceptSuccess); } else { toast.error(labels.error, { description: result.error }); } @@ -41,27 +39,23 @@ export function NftablesAlert({ agentId, detail, labels }: Props) {

{labels.title}

- {detail && ( -

- {detail} -

- )} + {detail &&

{detail}

}
diff --git a/lynx/dashboard/ui/src/components/(dashboard)/app/agents/RegisterAgentDialog.tsx b/lynx/dashboard/ui/src/components/(dashboard)/app/agents/RegisterAgentDialog.tsx index ccdbae1..2e7fa20 100644 --- a/lynx/dashboard/ui/src/components/(dashboard)/app/agents/RegisterAgentDialog.tsx +++ b/lynx/dashboard/ui/src/components/(dashboard)/app/agents/RegisterAgentDialog.tsx @@ -1,8 +1,9 @@ "use client"; -import { useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; +import { Plus } from "lucide-react"; import { useState } from "react"; +import { useForm } from "react-hook-form"; import { toast } from "sonner"; import { Button } from "@/components/ui/button"; import { @@ -14,11 +15,10 @@ import { DialogTitle, DialogTrigger, } from "@/components/ui/dialog"; +import { Field, FieldError, FieldLabel } from "@/components/ui/field"; import { Input } from "@/components/ui/input"; -import { Field, FieldLabel, FieldError } from "@/components/ui/field"; import { BACKEND_URL } from "@/lib/api"; -import { Plus } from "lucide-react"; -import { registerAgentSchema, type RegisterAgentInput } from "@/schemas/(dashboard)/app/agents"; +import { type RegisterAgentInput, registerAgentSchema } from "@/schemas/(dashboard)/app/agents"; type RegisteredAgent = { id: string; @@ -64,9 +64,9 @@ export function RegisterAgentDialog({ const onSubmit = (data: RegisterAgentInput) => { toast.promise( fetch(`${BACKEND_URL}/agents`, { - method: "POST", - headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` }, body: JSON.stringify({ name: data.name }), + headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" }, + method: "POST", }).then(async (res) => { if (!res.ok) throw new Error("failed"); const agent = (await res.json()) as RegisteredAgent; @@ -74,9 +74,9 @@ export function RegisterAgentDialog({ return agent; }), { + error: "Failed to register agent", loading: "Registering…", success: successTitle, - error: "Failed to register agent", }, ); }; @@ -88,7 +88,13 @@ export function RegisterAgentDialog({ } return ( - { if (!v) handleClose(); else setOpen(true); }}> + { + if (!v) handleClose(); + else setOpen(true); + }} + open={open} + > @@ -131,7 +137,7 @@ export function RegisterAgentDialog({
- +

{warnOnce}

@@ -154,7 +160,12 @@ function AgentField({ label, value, secret }: { label: string; value: string; se {revealed ? value : "•".repeat(Math.min(value.length, 32))} {secret && ( - )} diff --git a/lynx/dashboard/ui/src/components/(dashboard)/app/agents/detail/AgentDetailActions.tsx b/lynx/dashboard/ui/src/components/(dashboard)/app/agents/detail/AgentDetailActions.tsx index 49a705f..bacd041 100644 --- a/lynx/dashboard/ui/src/components/(dashboard)/app/agents/detail/AgentDetailActions.tsx +++ b/lynx/dashboard/ui/src/components/(dashboard)/app/agents/detail/AgentDetailActions.tsx @@ -1,25 +1,25 @@ "use client"; +import { RotateCcw, Trash2 } from "lucide-react"; import { useState, useTransition } from "react"; import { toast } from "sonner"; +import { deleteAgent, rebootAgent } from "@/actions/(dashboard)/app/agents"; import { Button } from "@/components/ui/button"; -import { RotateCcw, Trash2 } from "lucide-react"; -import { rebootAgent, deleteAgent } from "@/actions/(dashboard)/app/agents"; interface Labels { - reboot: string; - rebootConfirm: string; - rebootSuccess: string; - rebootError: string; deleteAgent: string; deleteConfirm: string; deleteError: string; + reboot: string; + rebootConfirm: string; + rebootError: string; + rebootSuccess: string; } interface Props { agentId: string; - locale: string; labels: Labels; + locale: string; } export function AgentDetailActions({ agentId, locale, labels }: Props) { @@ -52,21 +52,21 @@ export function AgentDetailActions({ agentId, locale, labels }: Props) { return (